diff --git a/README.md b/README.md index a8cbeacb..b470495c 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,16 @@ Automatically mirror TRaSH guides to your *darr instance. Features list will continue to grow. See the limitations & roadmap section for more details! -* Preferred, Must Not Contain, and Must Contain lists from guides are reflected completely in - corresponding fields in release profiles in Sonarr. -* "Include Preferred when Renaming" is properly checked/unchecked depending on explicit mention of - this in the guides. -* Profiles get created if they do not exist, or updated if they already exist. Profiles get a unique - name based on the guide and this name is used to find them in subsequent runs. -* Tags can be added to any updated or created profiles. +* Sonarr Release Profiles + * Preferred, Must Not Contain, and Must Contain lists from guides are reflected completely in + corresponding fields in release profiles in Sonarr. + * "Include Preferred when Renaming" is properly checked/unchecked depending on explicit mention of + this in the guides. + * Profiles get created if they do not exist, or updated if they already exist. Profiles get a unique + name based on the guide and this name is used to find them in subsequent runs. + * Tags can be added to any updated or created profiles. +* Sonarr Quality Definitions + * Anime and Non-Anime quality definitions are now synced to Sonarr ## Requirements @@ -26,21 +29,61 @@ Features list will continue to grow. See the limitations & roadmap section for m ## Getting Started -I plan to add more tutorials/details/instructions later, but for now just run `trash.py --help`: +The only script you will need to be using is `src/trash.py`. If you've cloned my repository, simply +`cd` to the `src` directory so you can run `trash.py` directly: ```txt -usage: trash.py [-h] [--preview] [--debug] base_uri api_key +PS E:\code\TrashUpdater\src> .\trash.py -h +usage: trash.py [-h] {profile,quality} ... -Automatically mirror TRaSH guides to your *darr instance. - -positional arguments: - base_uri The base URL for your Sonarr instance, for example `http://localhost:8989`. - api_key Your API key. +Automatically mirror TRaSH guides to your Sonarr/Radarr instance. optional arguments: - -h, --help show this help message and exit - --preview Only display the processed markdown results and nothing else. - --debug Display additional logs useful for development/debug purposes + -h, --help show this help message and exit + +subcommands: + Operations specific to different parts of the TRaSH guides + + {profile,quality} + profile Pages of the guide that define profiles + quality Pages in the guide that provide quality definitions +``` + +The command line is structured into a series of subcommands that each handle a different area of the +guides. For example, you use a separate subcommand to sync quality definitions than you do release +profiles. Simply run `trash.py [subcommand] -h` to get help for `[subcommand]`, which can be any +supported subcommand listed in the top level help output. + +### Examples + +Some command line examples to show you how to use the script for various tasks. Note that most +command line options were generated on a Windows environment, so you will see OS-specific syntax +(e.g. backslashes). Obviously Python works on Linux systems too, so adjust the examples as needed +for your platform. + +To preview what release profile information is parsed out of the Anime profile guide: + +```txt +.\trash.py profile sonarr:anime --preview +``` + +To sync the anime release profiles to your Sonarr instance: + +```txt +.\trash.py profile sonarr:anime --base-uri http://localhost:8989 --api-key a95cc792074644759fefe3ccab544f6e +``` + +To preview the Anime quality definition data parsed out of the Quality Definitions (file sizes) page +of the TRaSH guides: + +```txt +.\trash.py quality sonarr:anime --preview +``` + +Sync the non-anime quality definition to Sonarr: + +```txt +.\trash.py quality sonarr:non-anime --base-uri http://localhost:8989 --api-key a95cc792074644759fefe3ccab544f6e ``` ## Important Notices diff --git a/sonarr_api_examples/qualityProfile.update.json b/sonarr_api_examples/qualityProfile.update.json new file mode 100644 index 00000000..a6d54c51 --- /dev/null +++ b/sonarr_api_examples/qualityProfile.update.json @@ -0,0 +1,268 @@ +[{ + "quality": { + "id": 0, + "name": "Unknown", + "source": "unknown", + "resolution": 0 + }, + "title": "Unknown", + "weight": 1, + "minSize": 1.0, + "maxSize": 227.5, + "id": 1 + }, + { + "quality": { + "id": 1, + "name": "SDTV", + "source": "television", + "resolution": 480 + }, + "title": "SDTV", + "weight": 2, + "minSize": 2.0, + "maxSize": 100.0, + "id": 2 + }, + { + "quality": { + "id": 12, + "name": "WEBRip-480p", + "source": "webRip", + "resolution": 480 + }, + "title": "WEBRip-480p", + "weight": 3, + "minSize": 2.0, + "maxSize": 100.0, + "id": 3 + }, + { + "quality": { + "id": 8, + "name": "WEBDL-480p", + "source": "web", + "resolution": 480 + }, + "title": "WEBDL-480p", + "weight": 3, + "minSize": 2.0, + "maxSize": 100.0, + "id": 4 + }, + { + "quality": { + "id": 2, + "name": "DVD", + "source": "dvd", + "resolution": 480 + }, + "title": "DVD", + "weight": 4, + "minSize": 2.0, + "maxSize": 100.0, + "id": 5 + }, + { + "quality": { + "id": 13, + "name": "Bluray-480p", + "source": "bluray", + "resolution": 480 + }, + "title": "Bluray-480p", + "weight": 5, + "minSize": 2.0, + "maxSize": 100.0, + "id": 6 + }, + { + "quality": { + "id": 4, + "name": "HDTV-720p", + "source": "television", + "resolution": 720 + }, + "title": "HDTV-720p", + "weight": 6, + "minSize": 3.0, + "maxSize": 125.0, + "id": 7 + }, + { + "quality": { + "id": 9, + "name": "HDTV-1080p", + "source": "television", + "resolution": 1080 + }, + "title": "HDTV-1080p", + "weight": 7, + "minSize": 4.0, + "maxSize": 125.0, + "id": 8 + }, + { + "quality": { + "id": 10, + "name": "Raw-HD", + "source": "televisionRaw", + "resolution": 1080 + }, + "title": "Raw-HD", + "weight": 8, + "minSize": 4.0, + "id": 9 + }, + { + "quality": { + "id": 14, + "name": "WEBRip-720p", + "source": "webRip", + "resolution": 720 + }, + "title": "WEBRip-720p", + "weight": 9, + "minSize": 3.0, + "maxSize": 130.0, + "id": 10 + }, + { + "quality": { + "id": 5, + "name": "WEBDL-720p", + "source": "web", + "resolution": 720 + }, + "title": "WEBDL-720p", + "weight": 9, + "minSize": 3.0, + "maxSize": 130.0, + "id": 11 + }, + { + "quality": { + "id": 6, + "name": "Bluray-720p", + "source": "bluray", + "resolution": 720 + }, + "title": "Bluray-720p", + "weight": 10, + "minSize": 4.0, + "maxSize": 130.0, + "id": 12 + }, + { + "quality": { + "id": 15, + "name": "WEBRip-1080p", + "source": "webRip", + "resolution": 1080 + }, + "title": "WEBRip-1080p", + "weight": 11, + "minSize": 4.0, + "maxSize": 130.0, + "id": 13 + }, + { + "quality": { + "id": 3, + "name": "WEBDL-1080p", + "source": "web", + "resolution": 1080 + }, + "title": "WEBDL-1080p", + "weight": 11, + "minSize": 4.0, + "maxSize": 130.0, + "id": 14 + }, + { + "quality": { + "id": 7, + "name": "Bluray-1080p", + "source": "bluray", + "resolution": 1080 + }, + "title": "Bluray-1080p", + "weight": 12, + "minSize": 4.0, + "maxSize": 155.0, + "id": 15 + }, + { + "quality": { + "id": 20, + "name": "Bluray-1080p Remux", + "source": "blurayRaw", + "resolution": 1080 + }, + "title": "Bluray-1080p Remux", + "weight": 13, + "minSize": 35.0, + "id": 16 + }, + { + "quality": { + "id": 16, + "name": "HDTV-2160p", + "source": "television", + "resolution": 2160 + }, + "title": "HDTV-2160p", + "weight": 14, + "minSize": 35.0, + "maxSize": 199.9, + "id": 17 + }, + { + "quality": { + "id": 17, + "name": "WEBRip-2160p", + "source": "webRip", + "resolution": 2160 + }, + "title": "WEBRip-2160p", + "weight": 15, + "minSize": 59.0, + "id": 18 + }, + { + "quality": { + "id": 18, + "name": "WEBDL-2160p", + "source": "web", + "resolution": 2160 + }, + "title": "WEBDL-2160p", + "weight": 15, + "minSize": 59.0, + "id": 19 + }, + { + "quality": { + "id": 19, + "name": "Bluray-2160p", + "source": "bluray", + "resolution": 2160 + }, + "title": "Bluray-2160p", + "weight": 16, + "minSize": 59.0, + "id": 20 + }, + { + "quality": { + "id": 21, + "name": "Bluray-2160p Remux", + "source": "blurayRaw", + "resolution": 2160 + }, + "title": "Bluray-2160p Remux", + "weight": 17, + "minSize": 58.2, + "id": 21 + } +] diff --git a/src/app/__init__.py b/src/app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/app/api/sonarr.py b/src/app/api/sonarr.py index 5dba4c7e..f41c00f9 100644 --- a/src/app/api/sonarr.py +++ b/src/app/api/sonarr.py @@ -1,6 +1,7 @@ import requests import json from packaging import version # pip install packaging +from copy import deepcopy from .server import Server from ..profile_data import ProfileData @@ -13,6 +14,11 @@ class Sonarr(Server): self.logger = logger super().__init__(base_uri, key) + if not args.base_uri or not args.api_key: + raise ValueError('--base-uri and --api-key are required arguments when not using --preview') + + self.do_version_check() + # -------------------------------------------------------------------------------------------------- @staticmethod def get_error_message(response: requests.Response): @@ -94,3 +100,41 @@ class Sonarr(Server): current_tags_json.append(r) return current_tags_json + + # -------------------------------------------------------------------------------------------------- + def do_version_check(self): + # Since this script requires a specific version of v3 Sonarr that implements name support for + # release profiles, we perform that version check here and bail out if it does not meet a minimum + # required version. + minimum_version = version.parse('3.0.4.1098') + sonarr_version = self.get_version() + if sonarr_version < minimum_version: + raise RuntimeError(f'Your Sonarr version ({sonarr_version}) does not meet the minimum required version of {minimum_version} to use this script.') + exit(1) + + # -------------------------------------------------------------------------------------------------- + # GET /qualitydefinition + def get_quality_definition(self): + return self.request('get', '/qualitydefinition') + + # -------------------------------------------------------------------------------------------------- + # PUT /qualityDefinition/update + def update_quality_definition(self, sonarr_definition, guide_definition): + new_definition = deepcopy(sonarr_definition) + for quality, min, max in guide_definition: + entry = self.find_quality_definition_entry(new_definition, quality) + if not entry: + print(f'WARN: Quality definition lacks entry for {quality}; it will be skipped.') + continue + entry['minSize'] = min + entry['maxSize'] = max + + self.request('put', '/qualityDefinition/update', new_definition) + + # -------------------------------------------------------------------------------------------------- + def find_quality_definition_entry(self, new_definition, quality): + for entry in new_definition: + if entry.get('quality').get('name') == quality: + return entry + + return None \ No newline at end of file diff --git a/src/app/cmd.py b/src/app/cmd.py index e7584443..1e41cd8e 100644 --- a/src/app/cmd.py +++ b/src/app/cmd.py @@ -1,15 +1,51 @@ import argparse # class args: pass +class NoAction(argparse.Action): + def __init__(self, **kwargs): + kwargs.setdefault('default', argparse.SUPPRESS) + kwargs.setdefault('nargs', 0) + super(NoAction, self).__init__(**kwargs) -def setup_and_parse_args(): - argparser = argparse.ArgumentParser(description='Automatically mirror TRaSH guides to your *darr instance.') - argparser.add_argument('--preview', help='Only display the processed markdown results and nothing else.', + def __call__(self, parser, namespace, values, option_string=None): + pass + +def add_choices_argument(parser, variable_name, help_text, choices: dict): + parser.register('action', 'none', NoAction) + parser.add_argument(variable_name, help=help_text, metavar=variable_name.upper(), choices=choices.keys()) + group = parser.add_argument_group(title=f'Choices for {variable_name.upper()}') + for choice,choice_help in choices.items(): + group.add_argument(choice, help=choice_help, action='none') + +def setup_and_parse_args(args_override=None): + parent_p = argparse.ArgumentParser(add_help=False) + parent_p.add_argument('--base-uri', help='The base URL for your Sonarr/Radarr instance, for example `http://localhost:8989`. Required if not doing --preview.') + parent_p.add_argument('--api-key', help='Your API key. Required if not doing --preview.') + parent_p.add_argument('--preview', help='Only display the processed markdown results and nothing else.', action='store_true') - argparser.add_argument('--debug', help='Display additional logs useful for development/debug purposes', + parent_p.add_argument('--debug', help='Display additional logs useful for development/debug purposes', action='store_true') - argparser.add_argument('--tags', help='Tags to assign to the profiles that are created or updated. These tags will replace any existing tags when updating profiles.', + + parser = argparse.ArgumentParser(description='Automatically mirror TRaSH guides to your Sonarr/Radarr instance.') + subparsers = parser.add_subparsers(description='Operations specific to different parts of the TRaSH guides', dest='subcommand') + + # Subcommands for 'profile' + profile_p = subparsers.add_parser('profile', help='Pages of the guide that define profiles', + parents=[parent_p]) + add_choices_argument(profile_p, 'type', 'The specific guide type/page to pull data from.', { + 'sonarr:anime': 'The anime release profile for Sonarr v3', + 'sonarr:web-dl': 'The WEB-DL release profile for Sonarr v3' + }) + profile_p.add_argument('--tags', help='Tags to assign to the profiles that are created or updated. These tags will replace any existing tags when updating profiles.', nargs='+') - argparser.add_argument('base_uri', help='The base URL for your Sonarr instance, for example `http://localhost:8989`.') - argparser.add_argument('api_key', help='Your API key.') - return argparser.parse_args()#namespace=args) + + # Subcommands for 'quality' + quality_p = subparsers.add_parser('quality', help='Pages in the guide that provide quality definitions', + parents=[parent_p]) + add_choices_argument(quality_p, 'type', 'The specific guide type/page to pull data from.', { + 'sonarr:anime': 'Choose the Sonarr quality definition best fit for anime', + 'sonarr:non-anime': 'Choose the Sonarr quality definition best fit for tv shows (non-anime)', + 'sonarr:hybrid': 'The script will generate a Sonarr quality definition that works best for all show types' + }) + + return parser.parse_args(args=args_override) diff --git a/src/app/guide/quality.py b/src/app/guide/quality.py new file mode 100644 index 00000000..f8eaaf6e --- /dev/null +++ b/src/app/guide/quality.py @@ -0,0 +1,31 @@ +import requests +import re +from collections import defaultdict + +header_regex = re.compile(r'^#+') +table_row_regex = re.compile(r'\| *(.*?) *\| *([\d.]+) *\| *([\d.]+) *\|') + +# -------------------------------------------------------------------------------------------------- +def get_markdown(): + trash_anime_markdown_url = 'https://raw.githubusercontent.com/TRaSH-/Guides/master/docs/Sonarr/V3/Sonarr-Quality-Settings-File-Size.md' + response = requests.get(trash_anime_markdown_url) + return response.content.decode('utf8') + +# -------------------------------------------------------------------------------------------------- +def parse_markdown(logger, markdown_content): + results = defaultdict(list) + table = None + + for line in markdown_content.splitlines(): + if not line: + continue + + if header_regex.search(line): + category = 'sonarr:anime' if 'anime' in line.lower() else 'sonarr:non-anime' + table = results[category] + if len(table) > 0: + table = None + elif (match := table_row_regex.search(line)) and table is not None: + table.append((match.group(1), float(match.group(2)), float(match.group(3)))) + + return results diff --git a/src/app/guide/utils.py b/src/app/guide/utils.py index 83ba58e4..04404ad9 100644 --- a/src/app/guide/utils.py +++ b/src/app/guide/utils.py @@ -42,3 +42,12 @@ def find_existing_profile(profile_name, existing_profiles): if p.get('name') == profile_name: return p return None + +# -------------------------------------------------------------------------------------------------- +def quality_preview(name, definition): + print(name) + formats = ' {:<20} {:<10} {:<10}' + print(formats.format('Quality', 'Min', 'Max')) + for (quality, min, max) in definition: + print(formats.format(quality, min, max)) + print('') diff --git a/src/tests/__init__.py b/src/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/guide/data/test_parse_markdown_sonarr_quality_definitions.md b/src/tests/guide/data/test_parse_markdown_sonarr_quality_definitions.md new file mode 100644 index 00000000..5d184229 --- /dev/null +++ b/src/tests/guide/data/test_parse_markdown_sonarr_quality_definitions.md @@ -0,0 +1,39 @@ +## Sonarr Quality Definitions + +| Quality | Minimum | Maximum | +| ------------------ | ------- | ------- | +| HDTV-720p | 17.9 | 67.5 | +| HDTV-1080p | 20 | 137.3 | +| WEBRip-720p | 20 | 137.3 | +| WEBDL-720p | 20 | 137.3 | +| Bluray-720p | 34.9 | 137.3 | +| WEBRip-1080p | 22 | 137.3 | +| WEBDL-1080p | 22 | 137.3 | +| Bluray-1080p | 50.4 | 227 | +| Bluray-1080p Remux | 69.1 | 400 | +| HDTV-2160p | 84.5 | 350 | +| WEBRip-2160p | 84.5 | 350 | +| WEBDL-2160p | 84.5 | 350 | +| Bluray-2160p | 94.6 | 400 | +| Bluray-2160p Remux | 204.4 | 400 | + +------ + +### Sonarr Quality Definitions - Anime (Work in Progress) + +| Quality | Minimum | Maximum | +| ------------------ | ------- | ------- | +| HDTV-720p | 2.3 | 51.4 | +| HDTV-1080p | 2.3 | 100 | +| WEBRip-720p | 4.3 | 100 | +| WEBDL-720p | 4.3 | 51.4 | +| Bluray-720p | 4.3 | 102.2 | +| WEBRip-1080p | 4.5 | 257.4 | +| WEBDL-1080p | 4.3 | 253.6 | +| Bluray-1080p | 4.3 | 258.1 | +| Bluray-1080p Remux | 0 | 400 | +| HDTV-2160p | 84.5 | 350 | +| WEBRip-2160p | 84.5 | 350 | +| WEBDL-2160p | 84.5 | 350 | +| Bluray-2160p | 94.6 | 400 | +| Bluray-2160p Remux | 204.4 | 400 | \ No newline at end of file diff --git a/src/tests/guide/test_anime.py b/src/tests/guide/test_anime.py index 73750257..ab15255b 100644 --- a/src/tests/guide/test_anime.py +++ b/src/tests/guide/test_anime.py @@ -1,19 +1,15 @@ -import trash.guide.anime as anime +import app.guide.anime as anime from pathlib import Path +from tests.mock_logger import MockLogger data_files = Path(__file__).parent / 'data' -class TestLogger: - def info(self, msg): pass - - def debug(self, msg): pass - def test_parse_markdown_complete_doc(): md_file = data_files / 'test_parse_markdown_complete_doc.md' with open(md_file) as file: test_markdown = file.read() - results = anime.parse_markdown(TestLogger(), test_markdown) + results = anime.parse_markdown(MockLogger(), test_markdown) assert len(results) == 1 profile = next(iter(results.values())) diff --git a/src/tests/guide/test_quality.py b/src/tests/guide/test_quality.py new file mode 100644 index 00000000..277034a1 --- /dev/null +++ b/src/tests/guide/test_quality.py @@ -0,0 +1,55 @@ +import app.guide.quality as quality +from pathlib import Path +from tests.mock_logger import MockLogger + +data_files = Path(__file__).parent / 'data' + +def test_parse_markdown_complete_doc(): + md_file = data_files / 'test_parse_markdown_sonarr_quality_definitions.md' + with open(md_file) as file: + test_markdown = file.read() + + results = quality.parse_markdown(MockLogger(), test_markdown) + + # Dictionary: Key (header name (anime or non-anime)), list (quality definitions table rows) + assert len(results) == 2 + + table = results.get('sonarr:anime') + assert len(table) == 14 + table_expected = [ + ('HDTV-720p', 2.3, 51.4), + ('HDTV-1080p', 2.3, 100.0), + ('WEBRip-720p', 4.3, 100.0), + ('WEBDL-720p', 4.3, 51.4), + ('Bluray-720p', 4.3, 102.2), + ('WEBRip-1080p', 4.5, 257.4), + ('WEBDL-1080p', 4.3, 253.6), + ('Bluray-1080p', 4.3, 258.1), + ('Bluray-1080p Remux', 0.0, 400.0), + ('HDTV-2160p', 84.5, 350.0), + ('WEBRip-2160p', 84.5, 350.0), + ('WEBDL-2160p', 84.5, 350.0), + ('Bluray-2160p', 94.6, 400.0), + ('Bluray-2160p Remux', 204.4, 400.0) + ] + assert sorted(table) == sorted(table_expected) + + table = results.get('sonarr:non-anime') + assert len(table) == 14 + table_expected = [ + ('HDTV-720p', 17.9, 67.5), + ('HDTV-1080p', 20.0, 137.3), + ('WEBRip-720p', 20.0, 137.3), + ('WEBDL-720p', 20.0, 137.3), + ('Bluray-720p', 34.9, 137.3), + ('WEBRip-1080p', 22.0, 137.3), + ('WEBDL-1080p', 22.0, 137.3), + ('Bluray-1080p', 50.4, 227.0), + ('Bluray-1080p Remux', 69.1, 400.0), + ('HDTV-2160p', 84.5, 350.0), + ('WEBRip-2160p', 84.5, 350.0), + ('WEBDL-2160p', 84.5, 350.0), + ('Bluray-2160p', 94.6, 400.0), + ('Bluray-2160p Remux', 204.4, 400.0) + ] + assert sorted(table) == sorted(table_expected) diff --git a/src/tests/mock_logger.py b/src/tests/mock_logger.py new file mode 100644 index 00000000..d7fcf9b5 --- /dev/null +++ b/src/tests/mock_logger.py @@ -0,0 +1,3 @@ +class MockLogger: + def info(self, msg): pass + def debug(self, msg): pass \ No newline at end of file diff --git a/src/tests/test_trash.py b/src/tests/test_trash.py new file mode 100644 index 00000000..165bbfc4 --- /dev/null +++ b/src/tests/test_trash.py @@ -0,0 +1,19 @@ +import pytest +import sys +from pathlib import Path + +from app import cmd +from tests.mock_logger import MockLogger + +sys.path.insert(0, Path(__name__).parent.parent) +import trash + +class TestEntrypoint: + logger = MockLogger() + + @staticmethod + def test_throw_without_required_arguments(): + with pytest.raises(ValueError): + args = cmd.setup_and_parse_args(['profile', 'sonarr:anime', '--base-uri', 'value']) + trash.process_sonarr_profile(args, TestEntrypoint.logger) + diff --git a/src/trash.py b/src/trash.py index fcfc2502..706dbde6 100644 --- a/src/trash.py +++ b/src/trash.py @@ -1,69 +1,94 @@ import requests -from packaging import version # pip install packaging from app import guide from app.api.sonarr import Sonarr -from app.guide import anime, utils +from app.guide import anime, utils, quality from app.cmd import setup_and_parse_args from app.logger import Logger +def process_sonarr_profile(args, logger): + profiles = anime.parse_markdown(logger, anime.get_trash_anime_markdown()) + + # A few false-positive profiles are added sometimes. We filter these out by checking if they + # actually have meaningful data attached to them, such as preferred terms. If they are mostly empty, + # we remove them here. + utils.filter_profiles(profiles) + + if args.preview: + utils.print_terms_and_scores(profiles) + exit(0) + + sonarr = Sonarr(args, logger) + + # If tags were provided, ensure they exist. Tags that do not exist are added first, so that we + # may specify them with the release profile request payload. + tag_ids = [] + if args.tags: + tags = sonarr.get_tags() + tags = sonarr.create_missing_tags(tags, args.tags[:]) + logger.debug(f'Tags JSON: {tags}') + + # Get a list of IDs that we can pass along with the request to update/create release + # profiles + tag_ids = [t['id'] for t in tags if t['label'] in args.tags] + logger.debug(f'Tag IDs: {tag_ids}') + + # Obtain all of the existing release profiles first. If any were previously created by our script + # here, we favor replacing those instead of creating new ones, which would just be mostly duplicates + # (but with some differences, since there have likely been updates since the last run). + existing_profiles = sonarr.get_release_profiles() + + for name, profile in profiles.items(): + new_profile_name = f'[Trash] Anime - {name}' + profile_to_update = guide.utils.find_existing_profile(new_profile_name, existing_profiles) + + if profile_to_update: + print(f'Updating existing profile: {new_profile_name}') + sonarr.update_existing_profile(profile_to_update, profile, tag_ids) + else: + print(f'Creating new profile: {new_profile_name}') + sonarr.create_release_profile(new_profile_name, profile, tag_ids) + +def process_sonarr_quality(args, logger): + guide_definitions = quality.parse_markdown(logger, quality.get_markdown()) + + if args.type == 'sonarr:hybrid': + raise ValueError('Hybrid profile not implemented yet') + + selected_definition = guide_definitions.get(args.type) + + if args.preview: + utils.quality_preview(args.type, selected_definition) + exit(0) + + print(f'Updating quality definition using {args.type}') + sonarr = Sonarr(args, logger) + definition = sonarr.get_quality_definition() + sonarr.update_quality_definition(definition, selected_definition) + +def main(): + args = setup_and_parse_args() + logger = Logger(args) + if args.subcommand == 'profile': + if args.type.startswith('sonarr:'): + process_sonarr_profile(args, logger) + elif args.type.startswith('radarr:'): + raise NotImplementedError('Radarr guide support is not implemented yet') + + elif args.subcommand == 'quality': + if args.type.startswith('sonarr:'): + process_sonarr_quality(args, logger) + elif args.type.startswith('radarr:'): + raise NotImplementedError('Radarr quality support is not implemented yet') + if __name__ == '__main__': try: - args = setup_and_parse_args() - logger = Logger(args) - sonarr = Sonarr(args, logger) - - profiles = anime.parse_markdown(logger, anime.get_trash_anime_markdown()) - - # A few false-positive profiles are added sometimes. We filter these out by checking if they - # actually have meaningful data attached to them, such as preferred terms. If they are mostly empty, - # we remove them here. - utils.filter_profiles(profiles) - - if args.preview: - utils.print_terms_and_scores(profiles) - exit(0) - - # Since this script requires a specific version of v3 Sonarr that implements name support for - # release profiles, we perform that version check here and bail out if it does not meet a minimum - # required version. - minimum_version = version.parse('3.0.4.1098') - version = sonarr.get_version() - if version < minimum_version: - print(f'ERROR: Your Sonarr version ({version}) does not meet the minimum required version of {minimum_version} to use this script.') - exit(1) - - # If tags were provided, ensure they exist. Tags that do not exist are added first, so that we - # may specify them with the release profile request payload. - tag_ids = [] - if args.tags: - tags = sonarr.get_tags() - tags = sonarr.create_missing_tags(tags, args.tags[:]) - logger.debug(f'Tags JSON: {tags}') - - # Get a list of IDs that we can pass along with the request to update/create release - # profiles - tag_ids = [t['id'] for t in tags if t['label'] in args.tags] - logger.debug(f'Tag IDs: {tag_ids}') - - # Obtain all of the existing release profiles first. If any were previously created by our script - # here, we favor replacing those instead of creating new ones, which would just be mostly duplicates - # (but with some differences, since there have likely been updates since the last run). - existing_profiles = sonarr.get_release_profiles() - - for name, profile in profiles.items(): - new_profile_name = f'[Trash] Anime - {name}' - profile_to_update = guide.utils.find_existing_profile(new_profile_name, existing_profiles) - - if profile_to_update: - print(f'Updating existing profile: {new_profile_name}') - sonarr.update_existing_profile(profile_to_update, profile, tag_ids) - else: - print(f'Creating new profile: {new_profile_name}') - sonarr.create_release_profile(new_profile_name, profile, tag_ids) - + main() except requests.exceptions.HTTPError as e: print(e) if error_msg := Sonarr.get_error_message(e.response): print(f'Response Message: {error_msg}') exit(1) + except Exception as e: + print(f'ERROR: {e}') + exit(1) \ No newline at end of file