Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@

**<span style="color:#56adda">0.0.4</span>**
- Add ability to trigger Sonarr file renaming
- Improve logging
- Improve error handling

**<span style="color:#56adda">0.0.3</span>**
- Improve logging
- Improve description
Expand Down
73 changes: 41 additions & 32 deletions description.md
Original file line number Diff line number Diff line change
@@ -1,50 +1,59 @@

### Config description:
---

##### Links:

#### <span style="color:blue">Sonarr LAN IP Address</span>
The protocol and IP address of the Sonarr application
- [Support](https://unmanic.app/discord)
- [Issues/Feature Requests](https://github.com/Unmanic/plugin.notify_sonarr/issues)
- [Pull Requests](https://github.com/Unmanic/plugin.notify_sonarr/pulls)

---

##### Plugin Settings:

###### <span style="color:blue">Sonarr LAN IP Address
</span>
The protocol and IP address of the Sonarr application

#### <span style="color:blue">Sonarr API Key</span>
###### <span style="color:blue">Sonarr API Key</span>
Sonarr application API key

###### <span style="color:blue">Mode</span>
There are x2 modes available. Each mode has a different set of configuration options available to it.

- **Trigger series refresh on task complete**

Use this mode when you wish to simply trigger a refresh on a series to re-read a modified file after Unmanic has processed it.

#### <span style="color:blue">Mode</span>
Configuration options:
- ###### <span style="color:blue">Trigger Sonarr file renaming</span>

##### Trigger series refresh on task complete
Use this mode when you wish to simply trigger a refresh on a series to re-read a modified file after Unmanic has
processed it.
Trigger Sonarr to re-name files according to the defined naming scheme.

##### Import episode on task complete
Use this mode when you are running Unmanic prior to importing a file into Sonarr.
This will trigger a download import.
Useful if you've changed encodings and have these encodings in your Sonarr name templates.

If possible, this will associate with a matching queued download and import the file that way. However, it is possibly this will fail.
If it does fail, it will fallback to providing the file path to Sonarr and allowing Sonarr to carry out a normal
automated import by parsing the file name.
Only available if the *Trigger movie refresh on task complete* mode is selected.

<div style="background-color:pink;border-radius:4px;border-left:solid 5px red;padding:10px;">
<b>Warning:</b>
<br>When configuring a library that will be using this plugin in this "import" mode, it is advised to not include
the temporary download location within the library path. This may cause Unmanic to collect the incomplete
download especially with file monitor enabled.
</div>
- **Import episode on task complete**

#### <span style="color:blue">Limit file import size</span>
Only available if the *Import episode on task complete* mode is selected.
Use this mode when you are running Unmanic **prior** to importing a file into Sonarr. This will trigger a download import.

Enable limiting the Sonarr notification on items over the value specified in the *Minimum file size* option.
If possible, this will associate with a matching queued download and import the file that way. However, it is possible this will fail. If it does fail, it will fallback to providing the file path to Sonarr and allowing Sonarr to carry out a normal automated import by parsing the file name.

:::warning
When configuring a library that will be using this plugin in this "import" mode, it is advised to not include the temporary download location within the library path. This may cause Unmanic to collect the incomplete download especially with file monitor enabled.
:::

Configuration options:
- ###### <span style="color:blue">Limit file import size</span>

#### <span style="color:blue">Minimum file size</span>
Only available if the *Import episode on task complete* mode, and the *Limit file import size*
box is selected.
Enable limiting the Sonarr notification on items over the value specified in the *Minimum file size* option.

Sizes can be written as:
- ###### <span style="color:blue">Minimum file size</span>

- Bytes (Eg. '<span style="color:blue">50</span>' or '<span style="color:blue">800 B</span>')
- Kilobytes (Eg. '<span style="color:blue">100KB</span>' or '<span style="color:blue">23 K</span>')
- Megabytes (Eg. '<span style="color:blue">9M</span>' or '<span style="color:blue">34 MB</span>')
- Gigabytes (Eg. '<span style="color:blue">4GB</span>')
- etc...
Sizes can be written as:
- Bytes (Eg. '<span style="color:blue">50</span>' or '<span style="color:blue">800 B</span>')
- Kilobytes (Eg. '<span style="color:blue">100KB</span>' or '<span style="color:blue">23 K</span>')
- Megabytes (Eg. '<span style="color:blue">9M</span>' or '<span style="color:blue">34 MB</span>')
- Gigabytes (Eg. '<span style="color:blue">4GB</span>')
- etc...
2 changes: 1 addition & 1 deletion info.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@
"on_postprocessor_task_results": 0
},
"tags": "sonarr",
"version": "0.0.3"
"version": "0.0.4"
}
103 changes: 87 additions & 16 deletions plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,17 @@
import logging
import os
import pprint
import time

import humanfriendly
from pyarr import SonarrAPI
from pyarr.exceptions import (
PyarrAccessRestricted,
PyarrBadGateway,
PyarrConnectionError,
PyarrResourceNotFound,
PyarrUnauthorizedError,
)
from unmanic.libs.unplugins.settings import PluginSettings

# Configure plugin logger
Expand All @@ -38,6 +46,7 @@ class Settings(PluginSettings):
'host_url': 'http://localhost:8989',
'api_key': '',
'mode': 'update_mode',
'rename_files': False,
'limit_import_on_file_size': True,
'minimum_file_size': '100MB',
}
Expand Down Expand Up @@ -65,9 +74,17 @@ def __init__(self, *args, **kwargs):
},
],
},
"rename_files": self.__set_rename_files(),
"limit_import_on_file_size": self.__set_limit_import_on_file_size(),
"minimum_file_size": self.__set_minimum_file_size(),
}
def __set_rename_files(self):
values = {
"label": "Trigger Sonarr file renaming",
}
if self.get_setting('mode') != 'update_mode':
values["display"] = 'hidden'
return values

def __set_limit_import_on_file_size(self):
values = {
Expand Down Expand Up @@ -95,7 +112,7 @@ def check_file_size_under_max_file_size(path, minimum_file_size):
return True


def update_mode(api, dest_path):
def update_mode(api, dest_path, rename_files):
basename = os.path.basename(dest_path)

# Fetch episode data
Expand All @@ -105,16 +122,69 @@ def update_mode(api, dest_path):
series_title = episode_data.get('series', {}).get('title')
series_id = episode_data.get('series', {}).get('id')
if not series_id:
logger.error("Missing series ID. Failed to queued refresh of series for file: '{}'".format(dest_path))
logger.error("Missing series ID. Failed to queued refresh of series for file: '%s'", dest_path)
return

# Run API command for RescanSeries
# - RescanSeries with a series ID
result = api.post_command('RescanSeries', seriesId=series_id)
if result.get('message'):
logger.error("Failed to queued refresh of series ID '{}' for file: '{}'".format(series_id, dest_path))
try:
# Run API command for RescanSeries
# - RescanSeries with a series ID
result = api.post_command('RescanSeries', seriesId=series_id)
if result.get('message'):
logger.error("Failed to queue refresh of series ID '%s' for file: '%s'", series_id, dest_path)
logger.error("Response from sonarr: %s", result['message'])
return
else:
logger.info("Successfully queued refresh of the Series '%s' for file: '%s'", series_id, dest_path)
except PyarrUnauthorizedError:
logger.error("Failed to queue refresh of series ID '%s' for file: '%s'", series_id, dest_path)
logger.error("Unauthorized. Please ensure valid API Key is used.")
return
except PyarrAccessRestricted:
logger.error("Failed to queue refresh of series ID '%s' for file: '%s'", series_id, dest_path)
logger.error("Access restricted. Please ensure API Key has correct permissions")
return
logger.info("Successfully queued refreshed the Series '{}' for file: '{}'".format(series_title, dest_path))
except PyarrResourceNotFound:
logger.error("Failed to queue refresh of series ID '%s' for file: '%s'", series_id, dest_path)
logger.error("Resource not found")
return
except PyarrBadGateway:
logger.error("Failed to queue refresh of series ID '%s' for file: '%s'", series_id, dest_path)
logger.error("Bad Gateway. Check your server is accessible")
return
except PyarrConnectionError:
logger.error("Failed to queued refresh of series ID '%s' for file: '%s'", series_id, dest_path)
logger.error("Timeout connecting to sonarr. Check your server is accessible")
return

if rename_files:
time.sleep(10) # Must give time (more than Radarr) for the refresh to complete before we run the rename.
try:
# Bulk series rename is broken, so instead simulate the UI API call workflow.
rename_list = api.request_get("rename", "", params={"seriesId": {series_id}})
file_ids = [episode['episodeFileId'] for episode in rename_list]

result = api.post_command('RenameFiles', seriesId=series_id, files=file_ids)
if isinstance(result, dict):
logger.info("Successfully triggered rename of series '%s' for file: '%s'", series_title, dest_path)
else:
logger.error("Failed to trigger rename of series ID '%s' for file: '%s'", series_id, dest_path)
except PyarrUnauthorizedError:
logger.error("Failed to trigger rename of series '%s' for file: '%s'", series_title, dest_path)
logger.error("Unauthorized. Please ensure valid API Key is used.")
except PyarrAccessRestricted:
logger.error("Failed to trigger rename of series '%s' for file: '%s'", series_title, dest_path)
logger.error("Access restricted. Please ensure API Key has correct permissions")
except PyarrResourceNotFound:
logger.error("Failed to trigger rename of series '%s' for file: '%s'", series_title, dest_path)
logger.error("Resource not found")
except PyarrBadGateway:
logger.error("Failed to trigger rename of series '%s' for file: '%s'", series_title, dest_path)
logger.error("Bad Gateway. Check your server is accessible")
except PyarrConnectionError:
logger.error("Failed to trigger rename of series '%s' for file: '%s'", series_title, dest_path)
logger.error("Timeout connecting to sonarr. Check your server is accessible")
except BaseException as err:
logger.error("Failed to trigger rename of series ID '%s' for file: '%s'\nError received: %s", series_id, dest_path, str(err))


def import_mode(api, source_path, dest_path):
Expand All @@ -126,7 +196,7 @@ def import_mode(api, source_path, dest_path):

queue = api.get_queue()
message = pprint.pformat(queue, indent=1)
logger.debug("Current queue \n{}".format(message))
logger.debug("Current queue \n%s", message)
for item in queue.get('records', []):
item_output_basename = os.path.basename(item.get('outputPath'))
if item_output_basename == source_basename:
Expand All @@ -138,40 +208,41 @@ def import_mode(api, source_path, dest_path):
if download_id:
# Run API command for DownloadedEpisodesScan
# - DownloadedEpisodesScan with a path and downloadClientId
logger.info("Queued import episode '{}' using downloadClientId: '{}'".format(episode_title, download_id))
logger.info("Queued import episode '%s' using downloadClientId: '%s'", episode_title, download_id)
result = api.post_command('DownloadedEpisodesScan', path=abspath_string, downloadClientId=download_id)
else:
# Run API command for DownloadedEpisodesScan without passing a downloadClientId
# - DownloadedEpisodesScan with a path and downloadClientId
logger.info("Queued import using just the file path '{}'".format(abspath_string))
logger.info("Queued import using just the file path '%s'", abspath_string)
result = api.post_command('DownloadedEpisodesScan', path=abspath_string)

# Log results
message = result
if isinstance(result, dict) or isinstance(result, list):
message = pprint.pformat(result, indent=1)
logger.debug("Queued import result \n{}".format(message))
logger.debug("Queued import result \n%s", message)
if (isinstance(result, dict)) and result.get('message'):
logger.error("Failed to queued import of file: '{}'".format(dest_path))
logger.error("Failed to queued import of file: '%s'", dest_path)
return
# TODO: Check for other possible outputs
logger.info("Successfully queued import of file: '{}'".format(dest_path))
logger.info("Successfully queued import of file: '%s'", dest_path)


def process_files(settings, source_file, destination_files, host_url, api_key):
api = SonarrAPI(host_url, api_key)

mode = settings.get_setting('mode')
rename_files = settings.get_setting('rename_files')

# Get the basename of the file
for dest_file in destination_files:
if mode == 'update_mode':
update_mode(api, dest_file)
update_mode(api, dest_file, rename_files)
elif mode == 'import_mode':
minimum_file_size = settings.get_setting('minimum_file_size')
if check_file_size_under_max_file_size(dest_file, minimum_file_size):
# Ignore this file
logger.info("Ignoring file as it is under configured minimum size file: '{}'".format(dest_file))
logger.info("Ignoring file as it is under configured minimum size file: '%s'", dest_file)
continue
import_mode(api, source_file, dest_file)

Expand Down