diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..bbd74e7 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @santiagosayshey \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..63cbe00 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,27 @@ +name: Run Tests + +on: + push: + branches: [dev] + pull_request: + branches: [dev] + +jobs: + run-tests: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.9 + + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + pip install colorama + + - name: Run regex tests + run: python3 tests/regex.py \ No newline at end of file diff --git a/db/custom_formats/audio/ATMOS (Missing).json b/db/custom_formats/audio/ATMOS (Missing).json new file mode 100644 index 0000000..5af24d0 --- /dev/null +++ b/db/custom_formats/audio/ATMOS (Missing).json @@ -0,0 +1,69 @@ +{ + "name": "ATMOS (Missing)", + "includeCustomFormatWhenRenaming": false, + "specifications": [ + { + "name": "TrueHD", + "implementation": "ReleaseTitleSpecification", + "implementationName": "Release Title", + "infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2", + "negate": false, + "required": true, + "fields": [ + { + "order": 0, + "name": "value", + "label": "Regular Expression", + "helpText": "Custom Format RegEx is Case Insensitive", + "value": "True[ .-]?HD[ .-]?7\\.1", + "type": "textbox", + "advanced": false, + "privacy": "normal", + "isFloat": false + } + ] + }, + { + "name": "Atmos", + "implementation": "ReleaseTitleSpecification", + "implementationName": "Release Title", + "infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2", + "negate": true, + "required": true, + "fields": [ + { + "order": 0, + "name": "value", + "label": "Regular Expression", + "helpText": "Custom Format RegEx is Case Insensitive", + "value": "\\bATMOS(\\b|\\d)", + "type": "textbox", + "advanced": false, + "privacy": "normal", + "isFloat": false + } + ] + }, + { + "name": "7.1", + "implementation": "ReleaseTitleSpecification", + "implementationName": "Release Title", + "infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2", + "negate": false, + "required": true, + "fields": [ + { + "order": 0, + "name": "value", + "label": "Regular Expression", + "helpText": "Custom Format RegEx is Case Insensitive", + "value": "7.1", + "type": "textbox", + "advanced": false, + "privacy": "normal", + "isFloat": false + } + ] + } + ] +} diff --git a/db/custom_formats/audio/ATMOS.json b/db/custom_formats/audio/ATMOS.json new file mode 100644 index 0000000..2ecb3e3 --- /dev/null +++ b/db/custom_formats/audio/ATMOS.json @@ -0,0 +1,27 @@ +{ + "name": "ATMOS", + "includeCustomFormatWhenRenaming": true, + "specifications": [ + { + "name": "ATMOS", + "implementation": "ReleaseTitleSpecification", + "implementationName": "Release Title", + "infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2", + "negate": false, + "required": true, + "fields": [ + { + "order": 0, + "name": "value", + "label": "Regular Expression", + "helpText": "Custom Format RegEx is Case Insensitive", + "value": "\\bATMOS(\\b|\\d)", + "type": "textbox", + "advanced": false, + "privacy": "normal", + "isFloat": false + } + ] + } + ] +} diff --git a/db/custom_formats/audio/DD+.json b/db/custom_formats/audio/DD+.json new file mode 100644 index 0000000..dabcc2d --- /dev/null +++ b/db/custom_formats/audio/DD+.json @@ -0,0 +1,132 @@ +{ + "name": "DD+", + "includeCustomFormatWhenRenaming": false, + "specifications": [ + { + "name": "Dolby Digital Plus", + "implementation": "ReleaseTitleSpecification", + "implementationName": "Release Title", + "infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2", + "negate": false, + "required": true, + "fields": [ + { + "order": 0, + "name": "value", + "label": "Regular Expression", + "helpText": "Custom Format RegEx is Case Insensitive", + "value": "\\bDD[P+](?!A)|\\b(e[-_. ]?ac3)\\b", + "type": "textbox", + "advanced": false, + "privacy": "normal", + "isFloat": false + } + ] + }, + { + "name": "TrueHD", + "implementation": "ReleaseTitleSpecification", + "implementationName": "Release Title", + "infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2", + "negate": true, + "required": true, + "fields": [ + { + "order": 0, + "name": "value", + "label": "Regular Expression", + "helpText": "Custom Format RegEx is Case Insensitive", + "value": "True[ .-]?HD", + "type": "textbox", + "advanced": false, + "privacy": "normal", + "isFloat": false + } + ] + }, + { + "name": "Not DTS", + "implementation": "ReleaseTitleSpecification", + "implementationName": "Release Title", + "infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2", + "negate": true, + "required": true, + "fields": [ + { + "order": 0, + "name": "value", + "label": "Regular Expression", + "helpText": "Custom Format RegEx is Case Insensitive", + "value": "\\bDTS(\\b|\\d)", + "type": "textbox", + "advanced": false, + "privacy": "normal", + "isFloat": false + } + ] + }, + { + "name": "Not FLAC", + "implementation": "ReleaseTitleSpecification", + "implementationName": "Release Title", + "infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2", + "negate": true, + "required": true, + "fields": [ + { + "order": 0, + "name": "value", + "label": "Regular Expression", + "helpText": "Custom Format RegEx is Case Insensitive", + "value": "\\bFLAC(\\b|\\d)", + "type": "textbox", + "advanced": false, + "privacy": "normal", + "isFloat": false + } + ] + }, + { + "name": "Not AAC", + "implementation": "ReleaseTitleSpecification", + "implementationName": "Release Title", + "infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2", + "negate": true, + "required": true, + "fields": [ + { + "order": 0, + "name": "value", + "label": "Regular Expression", + "helpText": "Custom Format RegEx is Case Insensitive", + "value": "\\bAAC(\\b|\\d)", + "type": "textbox", + "advanced": false, + "privacy": "normal", + "isFloat": false + } + ] + }, + { + "name": "Not PCM", + "implementation": "ReleaseTitleSpecification", + "implementationName": "Release Title", + "infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2", + "negate": true, + "required": true, + "fields": [ + { + "order": 0, + "name": "value", + "label": "Regular Expression", + "helpText": "Custom Format RegEx is Case Insensitive", + "value": "\\b(l?)PCM(\\b|\\d)", + "type": "textbox", + "advanced": false, + "privacy": "normal", + "isFloat": false + } + ] + } + ] +} diff --git a/db/custom_formats/audio/DD.json b/db/custom_formats/audio/DD.json new file mode 100644 index 0000000..768751b --- /dev/null +++ b/db/custom_formats/audio/DD.json @@ -0,0 +1,153 @@ +{ + "name": "DD", + "includeCustomFormatWhenRenaming": true, + "specifications": [ + { + "name": "Basic Dolby Digital", + "implementation": "ReleaseTitleSpecification", + "implementationName": "Release Title", + "infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2", + "negate": false, + "required": true, + "fields": [ + { + "order": 0, + "name": "value", + "label": "Regular Expression", + "helpText": "Custom Format RegEx is Case Insensitive", + "value": "\\bDD[^a-z+]|(? /dev/null +then + echo "jq could not be found. Please install jq before running this script." + exit 1 +fi + +# Check if input file is provided as an argument +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +# Input JSON file +input_file="$1" + +# Check if the input file exists +if [[ ! -f "$input_file" ]]; then + echo "Input file not found: $input_file" + exit 1 +fi + +# Create output directory if it doesn't exist +output_dir="../output" +mkdir -p "$output_dir" + +# Function to sanitize filenames by removing slashes +sanitize_filename() { + echo "$1" | tr -d '/' +} + +# Read each object in the array and save to a separate JSON file +jq -c '.[]' "$input_file" | while read -r object; do + name=$(echo "$object" | jq -r '.name') + if [[ -n "$name" ]]; then + sanitized_name=$(sanitize_filename "$name") + echo "$object" | jq '.' > "$output_dir/$sanitized_name.json" + echo "Created file: $output_dir/$sanitized_name.json" + else + echo "Skipping object without a name field: $object" + fi +done + +echo "Finished processing JSON objects." diff --git a/tests/regex.py b/tests/regex.py new file mode 100755 index 0000000..f8244d7 --- /dev/null +++ b/tests/regex.py @@ -0,0 +1,152 @@ +import json +import subprocess +import time +import re +import os +import sys +from colorama import Fore, Style + +class RegexTester: + def __init__(self, max_retries=3, retry_delay=1): + self.max_retries = max_retries + self.retry_delay = retry_delay + + def get_latest_revision(self, unique_id): + url = f"https://regex101.com/api/regex/{unique_id}" + retries = 0 + while retries < self.max_retries: + try: + response = subprocess.check_output(["curl", "-s", url]) + data = json.loads(response.decode("utf-8")) + revisions = data["versions"] + latest_revision = revisions[-1] + return latest_revision + except (subprocess.CalledProcessError, json.JSONDecodeError) as e: + print(f"{Fore.YELLOW}Warning: {str(e)}. Retrying in {self.retry_delay} second(s)...{Style.RESET_ALL}") + retries += 1 + time.sleep(self.retry_delay) + raise Exception("Failed to retrieve latest revision after multiple retries.") + + def get_regex_data(self, unique_id, revision): + url = f"https://regex101.com/api/regex/{unique_id}/{revision}" + retries = 0 + while retries < self.max_retries: + try: + response = subprocess.check_output(["curl", "-s", url]) + data = json.loads(response.decode("utf-8")) + regex_pattern = data["regex"] + unit_tests = data["unitTests"] + flavor = data["flavor"] + return regex_pattern, unit_tests, flavor + except (subprocess.CalledProcessError, json.JSONDecodeError) as e: + print(f"{Fore.YELLOW}Warning: {str(e)}. Retrying in {self.retry_delay} second(s)...{Style.RESET_ALL}") + retries += 1 + time.sleep(self.retry_delay) + raise Exception("Failed to retrieve regex data after multiple retries.") + + def run_unit_tests(self, regex_pattern, flavor, unit_tests): + success_count = 0 + failure_count = 0 + if flavor == "pcre2": + regex_flags = re.MULTILINE | re.IGNORECASE | re.DOTALL + else: + regex_flags = re.MULTILINE | re.IGNORECASE + + for test in unit_tests: + test_string = test["testString"] + criteria = test["criteria"] + target = test["target"] + + if target == "REGEX": + matches = re.finditer(regex_pattern, test_string, regex_flags) + match_found = bool(list(matches)) + + if criteria == "DOES_MATCH": + if match_found: + success_count += 1 + print(f"{Fore.GREEN}Passed test{Style.RESET_ALL}: '{test_string}'") + else: + failure_count += 1 + print(f"{Fore.RED}Should match{Style.RESET_ALL} '{test_string}'") + elif criteria == "DOES_NOT_MATCH": + if not match_found: + success_count += 1 + print(f"{Fore.GREEN}Passed test{Style.RESET_ALL}: '{test_string}'") + else: + failure_count += 1 + print(f"{Fore.RED}Shouldn't match{Style.RESET_ALL} '{test_string}'") + + return success_count, failure_count + +def extract_regex_ids(data): + regex_ids = {} + + def extract_ids(obj): + if isinstance(obj, dict): + if "fields" in obj: + for field in obj["fields"]: + if "regexID" in field: + regex_id = field["regexID"] + name = obj.get("name", "") + regex_ids[regex_id] = name + for value in obj.values(): + extract_ids(value) + elif isinstance(obj, list): + for item in obj: + extract_ids(item) + + extract_ids(data) + return regex_ids + +def main(): + tester = RegexTester() + regex_dir = "./db/custom_formats" + tested_ids = set() + any_test_failed = False + + for root, dirs, files in os.walk(regex_dir): + for filename in files: + if filename.endswith(".json"): + filepath = os.path.join(root, filename) + with open(filepath, "r") as f: + data = json.load(f) + + regex_ids = extract_regex_ids(data) + + for regex_id, name in regex_ids.items(): + if regex_id not in tested_ids: + tested_ids.add(regex_id) + try: + latest_revision = tester.get_latest_revision(regex_id) + regex_pattern, unit_tests, flavor = tester.get_regex_data(regex_id, latest_revision) + print(f"====================") + print(f"Running Test: {Fore.YELLOW}{name}{Style.RESET_ALL}") + print(f"Regex: {Fore.YELLOW}{regex_pattern}{Style.RESET_ALL}") + print(f"Link: {Fore.BLUE}https://regex101.com/r/{regex_id}{Style.RESET_ALL}") + print(f"====================") + print() + + total_tests = len(unit_tests) + passed_tests, failed_tests = tester.run_unit_tests(regex_pattern, flavor, unit_tests) + + score_color = Fore.GREEN if passed_tests == total_tests else Fore.RED + status_text = "PASS" if failed_tests == 0 else "FAIL" + status_color = Fore.GREEN if status_text == "PASS" else Fore.RED + + print() + print(f"====================") + print(f"Score: {score_color}{passed_tests} / {total_tests}{Style.RESET_ALL}") + print(f"Status: {status_color}{status_text}{Style.RESET_ALL}") + print(f"====================") + print() + + if failed_tests > 0: + sys.exit(1) + except Exception as e: + print(f"{Fore.RED}Error processing regex {regex_id}: {str(e)}{Style.RESET_ALL}") + print() + sys.exit(1) + sys.exit(0) + +if __name__ == "__main__": + main() \ No newline at end of file