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 pathlib import Path
|
||||||
from tests.mock_logger import MockLogger
|
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 pathlib import Path
|
||||||
from tests.mock_logger import MockLogger
|
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