parent
8c31968bfc
commit
3aad617219
@ -0,0 +1,63 @@
|
||||
import requests
|
||||
import json
|
||||
from copy import deepcopy
|
||||
|
||||
from app.api.server import Server, TrashHttpError
|
||||
from app.trash_error import TrashError
|
||||
|
||||
class RadarrHttpError(TrashHttpError):
|
||||
@staticmethod
|
||||
def get_error_message(response: requests.Response):
|
||||
content = json.loads(response.content)
|
||||
if len(content) > 0:
|
||||
if type(content) is list:
|
||||
return content[0]['errorMessage']
|
||||
elif type(content) is dict and 'message' in content:
|
||||
return content['message']
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
msg = f'HTTP Response Error [Status Code {self.response.status_code}] [URI: {self.response.url}]'
|
||||
if error_msg := RadarrHttpError.get_error_message(self.response):
|
||||
msg += f'\n Response Message: {error_msg}'
|
||||
return msg
|
||||
|
||||
class Radarr(Server):
|
||||
# --------------------------------------------------------------------------------------------------
|
||||
def __init__(self, args, logger):
|
||||
if not args.base_uri or not args.api_key:
|
||||
raise TrashError('--base-uri and --api-key are required arguments when not using --preview')
|
||||
|
||||
self.logger = logger
|
||||
|
||||
base_uri = f'{args.base_uri}/api/v3'
|
||||
key = f'?apikey={args.api_key}'
|
||||
super().__init__(base_uri, key, RadarrHttpError)
|
||||
|
||||
# --------------------------------------------------------------------------------------------------
|
||||
# GET /qualitydefinition
|
||||
def get_quality_definition(self):
|
||||
return self.request('get', '/qualitydefinition')
|
||||
|
||||
# --------------------------------------------------------------------------------------------------
|
||||
# PUT /qualityDefinition/update
|
||||
def update_quality_definition(self, server_definition, guide_definition):
|
||||
new_definition = deepcopy(server_definition)
|
||||
for quality, min, max, preferred 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
|
||||
entry['preferredSize'] = preferred
|
||||
|
||||
self.request('put', '/qualityDefinition/update', new_definition)
|
||||
|
||||
# --------------------------------------------------------------------------------------------------
|
||||
def find_quality_definition_entry(self, definition, quality):
|
||||
for entry in definition:
|
||||
if entry.get('quality').get('name') == quality:
|
||||
return entry
|
||||
|
||||
return None
|
@ -1 +0,0 @@
|
||||
from . import profile, quality, utils
|
@ -0,0 +1,13 @@
|
||||
# This defines general information specific to guide types. Used across different modules as needed.
|
||||
types = {
|
||||
'sonarr:anime': {
|
||||
'cmd_help': 'The anime release profile for Sonarr v3',
|
||||
'markdown_doc_name': 'Sonarr-Release-Profile-RegEx-Anime',
|
||||
'profile_typename': 'Anime'
|
||||
},
|
||||
'sonarr:web-dl': {
|
||||
'cmd_help': 'The WEB-DL release profile for Sonarr v3',
|
||||
'markdown_doc_name': 'Sonarr-Release-Profile-RegEx',
|
||||
'profile_typename': 'WEB-DL'
|
||||
},
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
# This defines general information specific to quality definition types. Used across different modules as needed.
|
||||
types = {
|
||||
'sonarr:anime': {
|
||||
'cmd_help': 'Choose the Sonarr quality definition best fit for anime'
|
||||
},
|
||||
'sonarr:non-anime': {
|
||||
'cmd_help': 'Choose the Sonarr quality definition best fit for tv shows (non-anime)'
|
||||
},
|
||||
'sonarr:hybrid': {
|
||||
'cmd_help': 'The script will generate a Sonarr quality definition that works best for all show types'
|
||||
},
|
||||
'radarr:movies': {
|
||||
'cmd_help': 'Choose the Radarr quality definition used for movies.'
|
||||
},
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
import requests
|
||||
import re
|
||||
from collections import defaultdict
|
||||
|
||||
header_regex = re.compile(r'^#+')
|
||||
table_row_regex = re.compile(r'\| *(.*?) *\| *([\d.]+) *\| *([\d.]+) *\|')
|
||||
|
||||
# --------------------------------------------------------------------------------------------------
|
||||
def get_markdown():
|
||||
markdown_page_url = 'https://raw.githubusercontent.com/TRaSH-/Guides/master/docs/Radarr/V3/Radarr-Quality-Settings-File-Size.md'
|
||||
response = requests.get(markdown_page_url)
|
||||
return response.content.decode('utf8')
|
||||
|
||||
# --------------------------------------------------------------------------------------------------
|
||||
def parse_markdown(args, logger, markdown_content):
|
||||
results = defaultdict(list)
|
||||
table = None
|
||||
|
||||
# Convert from 0-100 to 0.0-1.0
|
||||
preferred_ratio = args.preferred_percentage / 100
|
||||
|
||||
for line in markdown_content.splitlines():
|
||||
if not line:
|
||||
continue
|
||||
|
||||
if header_regex.search(line):
|
||||
category = args.type
|
||||
table = results[category]
|
||||
if len(table) > 0:
|
||||
table = None
|
||||
elif (match := table_row_regex.search(line)) and table is not None:
|
||||
quality = match.group(1)
|
||||
min = float(match.group(2))
|
||||
max = float(match.group(3))
|
||||
# TODO: Support reading preferred from table data in the guide
|
||||
preferred = round(min + (max-min) * preferred_ratio, 1)
|
||||
table.append((quality, min, max, preferred))
|
||||
|
||||
return results
|
@ -0,0 +1,54 @@
|
||||
# --------------------------------------------------------------------------------------------------
|
||||
# Filter out false-positive profiles that are empty.
|
||||
def filter_profiles(profiles):
|
||||
for name in list(profiles.keys()):
|
||||
profile = profiles[name]
|
||||
if not len(profile.required) and not len(profile.ignored) and not len(profile.preferred):
|
||||
del profiles[name]
|
||||
|
||||
# --------------------------------------------------------------------------------------------------
|
||||
def print_terms_and_scores(profiles):
|
||||
for name, profile in profiles.items():
|
||||
print(name)
|
||||
|
||||
if profile.include_preferred_when_renaming is not None:
|
||||
print(' Include Preferred when Renaming?')
|
||||
print(' ' + ('CHECKED' if profile.include_preferred_when_renaming else 'NOT CHECKED'))
|
||||
print('')
|
||||
|
||||
if len(profile.required):
|
||||
print(' Must Contain:')
|
||||
for term in profile.required:
|
||||
print(f' {term}')
|
||||
print('')
|
||||
|
||||
if len(profile.ignored):
|
||||
print(' Must Not Contain:')
|
||||
for term in profile.ignored:
|
||||
print(f' {term}')
|
||||
print('')
|
||||
|
||||
if len(profile.preferred):
|
||||
print(' Preferred:')
|
||||
for score, terms in profile.preferred.items():
|
||||
for term in terms:
|
||||
print(f' {score:<10} {term}')
|
||||
|
||||
print('')
|
||||
|
||||
# --------------------------------------------------------------------------------------------------
|
||||
def find_existing_profile(profile_name, existing_profiles):
|
||||
for p in existing_profiles:
|
||||
if p.get('name') == profile_name:
|
||||
return p
|
||||
return None
|
||||
|
||||
# --------------------------------------------------------------------------------------------------
|
||||
def quality_preview(definition):
|
||||
print('')
|
||||
formats = '{:<20} {:<10} {:<10} {:<10}'
|
||||
print(formats.format('Quality', 'Min', 'Max', 'Preferred'))
|
||||
print(formats.format('-------', '---', '---', '---'))
|
||||
for (quality, min, max, preferred) in definition:
|
||||
print(formats.format(quality, min, max, preferred))
|
||||
print('')
|
@ -0,0 +1 @@
|
||||
from . import profile, quality
|
@ -1 +1 @@
|
||||
from . import config, main, sonarr
|
||||
from . import config, main, sonarr, radarr
|
@ -0,0 +1,20 @@
|
||||
from app.guide.radarr import quality, utils
|
||||
from app.api.radarr import Radarr
|
||||
from app.trash_error import TrashError
|
||||
|
||||
# --------------------------------------------------------------------------------------------------
|
||||
def process_quality(args, logger):
|
||||
if 0 > args.preferred_percentage > 100:
|
||||
raise TrashError(f'Preferred percentage is out of range: {args.preferred_percentage}')
|
||||
|
||||
guide_definitions = quality.parse_markdown(args, logger, quality.get_markdown())
|
||||
selected_definition = guide_definitions.get(args.type)
|
||||
|
||||
if args.preview:
|
||||
utils.quality_preview(selected_definition)
|
||||
exit(0)
|
||||
|
||||
print(f'Updating quality definition using {args.type}')
|
||||
server = Radarr(args, logger)
|
||||
definition = server.get_quality_definition()
|
||||
server.update_quality_definition(definition, selected_definition)
|
@ -1,4 +1,4 @@
|
||||
from app.guide import profile
|
||||
from app.guide.sonarr import profile
|
||||
from pathlib import Path
|
||||
from tests.mock_logger import MockLogger
|
||||
|
@ -1,4 +1,4 @@
|
||||
import app.guide.quality as quality
|
||||
import app.guide.sonarr.quality as quality
|
||||
from pathlib import Path
|
||||
from tests.mock_logger import MockLogger
|
||||
|
@ -0,0 +1,13 @@
|
||||
import pytest
|
||||
|
||||
from app import cmd
|
||||
from app.logic import radarr
|
||||
from app.trash_error import TrashError
|
||||
from tests.mock_logger import MockLogger
|
||||
|
||||
@pytest.mark.parametrize('percentage', ['-1', '101'])
|
||||
def test_process_quality_bad_preferred_percentage(percentage):
|
||||
input_args = ['quality', 'radarr:movies', '--preferred-percentage', percentage]
|
||||
args = cmd.setup_and_parse_args(input_args)
|
||||
with pytest.raises(TrashError):
|
||||
radarr.process_quality(args, MockLogger())
|
Loading…
Reference in new issue