diff --git a/.idea/TrashUpdater.iml b/.idea/TrashUpdater.iml
index d35d5baf..72a80507 100644
--- a/.idea/TrashUpdater.iml
+++ b/.idea/TrashUpdater.iml
@@ -2,13 +2,22 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 00000000..a55e7a17
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index a155f9ee..a8cbeacb 100644
--- a/README.md
+++ b/README.md
@@ -71,3 +71,11 @@ In addition to the above limitations, the following items are planned for the fu
* Implement some sort of guide versioning (e.g. to avoid updating a release profile if the guide did
not change).
* Unit Testing
+
+## Development / Contributing
+
+### Prerequisites
+
+Some additional packages are required to run the unit tests. All can be installed via `pip`:
+
+* `pytest`
diff --git a/src/app/guide/anime.py b/src/app/guide/anime.py
index f5696bee..4d2928da 100644
--- a/src/app/guide/anime.py
+++ b/src/app/guide/anime.py
@@ -5,16 +5,16 @@ import requests
from ..profile_data import ProfileData
-Filter = Enum('FilterType', 'Preferred Required Ignored')
+TermCategory = Enum('TermCategory', 'Preferred Required Ignored')
header_regex = re.compile(r'^(#+)\s([\w\s\d]+)\s*$')
score_regex = re.compile(r'score.*?\[(-?[\d]+)\]', re.IGNORECASE)
# included_preferred_regex = re.compile(r'include preferred', re.IGNORECASE)
# not_regex = re.compile(r'not', re.IGNORECASE)
-filter_regexes = (
- (Filter.Required, re.compile(r'must contain', re.IGNORECASE)),
- (Filter.Ignored, re.compile(r'must not contain', re.IGNORECASE)),
- (Filter.Preferred, re.compile(r'preferred', re.IGNORECASE)),
+category_regex = (
+ (TermCategory.Required, re.compile(r'must contain', re.IGNORECASE)),
+ (TermCategory.Ignored, re.compile(r'must not contain', re.IGNORECASE)),
+ (TermCategory.Preferred, re.compile(r'preferred', re.IGNORECASE)),
)
# --------------------------------------------------------------------------------------------------
@@ -24,23 +24,21 @@ def get_trash_anime_markdown():
return response.content.decode('utf8')
# --------------------------------------------------------------------------------------------------
-def parse_filter(line):
- for rx in filter_regexes:
+def parse_category(line):
+ for rx in category_regex:
if rx[1].search(line):
return rx[0]
return None
# --------------------------------------------------------------------------------------------------
-def parse_markdown(logger):
- class state:
- results = defaultdict(ProfileData)
- profile_name = None
- score = None
- filter = None
- bracket_depth = 0
+def parse_markdown(logger, markdown_content):
+ results = defaultdict(ProfileData)
+ profile_name = None
+ score = None
+ category = None
+ bracket_depth = 0
- markdown_content = get_trash_anime_markdown()
for line in markdown_content.splitlines():
# Header processing
if match := header_regex.search(line):
@@ -49,41 +47,42 @@ def parse_markdown(logger):
# Profile name (always reset previous state here)
if header_depth == 3:
- state.score = None
- state.filter = Filter.Preferred
- state.profile_name = header_text
+ score = None
+ category = TermCategory.Preferred
+ profile_name = header_text
logger.debug(f'New Profile: {header_text}')
# Filter type for provided regexes
elif header_depth == 4:
- state.filter = parse_filter(header_text)
- if state.filter:
- logger.debug(f' Filter Set: {state.filter}')
+ category = parse_category(header_text)
+ if category:
+ logger.debug(f' Category Set: {category}')
# Lines we always look for
elif line.startswith('```'):
- state.bracket_depth = 1 - state.bracket_depth
+ bracket_depth = 1 - bracket_depth
- # Filter-based line processing
- elif state.profile_name:
- profile = state.results[state.profile_name]
+ # Category-based line processing
+ elif profile_name:
+ profile = results[profile_name]
lower_line = line.lower()
if 'include preferred' in lower_line:
profile.include_preferred_when_renaming = 'not' not in lower_line
logger.debug(f' Include preferred found: {profile.include_preferred_when_renaming}, {lower_line}')
- elif state.filter == Filter.Preferred:
+ elif category == TermCategory.Preferred:
if match := score_regex.search(line):
# bracket_depth = 0
- state.score = int(match.group(1))
- elif state.bracket_depth:
- if state.score is not None:
- logger.debug(f' [Preferred] Score: {state.score}, Term: {line}')
- profile.preferred[state.score].append(line)
- elif state.filter == Filter.Ignored:
- if state.bracket_depth:
- # Sometimes a comma is present at the end of these regexes, because when it's
- # pasted into Sonarr it acts as a delimiter. However, when using them with the
- # API we do not need them.
- profile.ignored.append(line.rstrip(','))
+ score = int(match.group(1))
+ elif bracket_depth:
+ if score is not None:
+ logger.debug(f' [Preferred] Score: {score}, Term: {line}')
+ profile.preferred[score].append(line)
+ elif category == TermCategory.Ignored and bracket_depth:
+ # Sometimes a comma is present at the end of these regexes, because when it's
+ # pasted into Sonarr it acts as a delimiter. However, when using them with the
+ # API we do not need them.
+ profile.ignored.append(line.rstrip(','))
+ elif category == TermCategory.Required and bracket_depth:
+ profile.required.append(line.rstrip(','))
logger.debug('\n\n')
- return state.results
+ return results
diff --git a/src/tests/guide/data/test_parse_markdown_complete_doc.md b/src/tests/guide/data/test_parse_markdown_complete_doc.md
new file mode 100644
index 00000000..eb1d23b6
--- /dev/null
+++ b/src/tests/guide/data/test_parse_markdown_complete_doc.md
@@ -0,0 +1,22 @@
+### Profile 1
+
+The score is [100]
+
+```
+term1
+```
+
+This is another Score that should not be used [200]
+
+#### Must not contain
+
+```
+term2
+term3
+```
+
+#### Must contain
+
+```
+term4
+```
\ No newline at end of file
diff --git a/src/tests/guide/test_anime.py b/src/tests/guide/test_anime.py
new file mode 100644
index 00000000..73750257
--- /dev/null
+++ b/src/tests/guide/test_anime.py
@@ -0,0 +1,28 @@
+import trash.guide.anime as anime
+from pathlib import Path
+
+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)
+
+ assert len(results) == 1
+ profile = next(iter(results.values()))
+
+ assert len(profile.ignored) == 2
+ assert sorted(profile.ignored) == sorted(['term2', 'term3'])
+
+ assert len(profile.required) == 1
+ assert profile.required == ['term4']
+
+ assert len(profile.preferred) == 1
+ assert profile.preferred.get(100) == ['term1']
diff --git a/src/trash.py b/src/trash.py
index c72b1986..fcfc2502 100644
--- a/src/trash.py
+++ b/src/trash.py
@@ -1,9 +1,4 @@
import requests
-# import re
-# import json
-# from collections import defaultdict
-# from dataclasses import dataclass
-# from enum import Enum
from packaging import version # pip install packaging
from app import guide
@@ -12,64 +7,63 @@ from app.guide import anime, utils
from app.cmd import setup_and_parse_args
from app.logger import Logger
-####################################################################################################
+if __name__ == '__main__':
+ try:
+ args = setup_and_parse_args()
+ logger = Logger(args)
+ sonarr = Sonarr(args, logger)
-try:
- args = setup_and_parse_args()
- logger = Logger(args)
- sonarr = Sonarr(args, logger)
+ profiles = anime.parse_markdown(logger, anime.get_trash_anime_markdown())
- profiles = anime.parse_markdown(logger)
+ # 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)
- # 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)
- 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)
- # 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}')
+ # 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}')
+ # 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()
+ # 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)
+ 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)
+ 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)
-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 requests.exceptions.HTTPError as e:
+ print(e)
+ if error_msg := Sonarr.get_error_message(e.response):
+ print(f'Response Message: {error_msg}')
+ exit(1)