From 14e713b4f445936a571d6d56d0f9e1cb8cc7d853 Mon Sep 17 00:00:00 2001 From: Mundokiir Date: Tue, 27 Sep 2022 19:14:31 -0700 Subject: [PATCH] Add file renaming --- changelog.md | 5 +++ description.md | 73 ++++++++++++++++++++--------------- info.json | 2 +- plugin.py | 103 +++++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 134 insertions(+), 49 deletions(-) diff --git a/changelog.md b/changelog.md index 8b7d982..05677be 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,9 @@ +**0.0.4** +- Add ability to trigger Sonarr file renaming +- Improve logging +- Improve error handling + **0.0.3** - Improve logging - Improve description diff --git a/description.md b/description.md index d2ac1af..62472d8 100644 --- a/description.md +++ b/description.md @@ -1,50 +1,59 @@ -### Config description: +--- +##### Links: -#### Sonarr LAN IP Address -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: +###### Sonarr LAN IP Address + +The protocol and IP address of the Sonarr application -#### Sonarr API Key +###### Sonarr API Key Sonarr application API key +###### Mode +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. -#### Mode + Configuration options: + - ###### Trigger Sonarr file renaming -##### 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. -
-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. -
+- **Import episode on task complete** -#### Limit file import size -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: + - ###### Limit file import size -#### Minimum file size -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: + - ###### Minimum file size -- Bytes (Eg. '50' or '800 B') -- Kilobytes (Eg. '100KB' or '23 K') -- Megabytes (Eg. '9M' or '34 MB') -- Gigabytes (Eg. '4GB') -- etc... + Sizes can be written as: + - Bytes (Eg. '50' or '800 B') + - Kilobytes (Eg. '100KB' or '23 K') + - Megabytes (Eg. '9M' or '34 MB') + - Gigabytes (Eg. '4GB') + - etc... \ No newline at end of file diff --git a/info.json b/info.json index 7f07670..6ebc67e 100644 --- a/info.json +++ b/info.json @@ -11,5 +11,5 @@ "on_postprocessor_task_results": 0 }, "tags": "sonarr", - "version": "0.0.3" + "version": "0.0.4" } \ No newline at end of file diff --git a/plugin.py b/plugin.py index 134188b..1f3efb7 100644 --- a/plugin.py +++ b/plugin.py @@ -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 @@ -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', } @@ -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 = { @@ -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 @@ -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): @@ -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: @@ -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)