From 2e5cabe7ab399053d3f1b6cd42b07a67b2bd83eb Mon Sep 17 00:00:00 2001 From: santiagosayshey Date: Sat, 3 Feb 2024 11:32:04 +1030 Subject: [PATCH] v0.2.4 ## Features - Implemented #12 - Enhanced message formatting and user prompts for deletions. ## Additions - ETHEL SCENE release group added. - FLiGHTS group to missing HDR10. - Overhauled h265 custom formats and added new h265 4k format. - MAX WEB source added to custom formats. ## Improvements - Script to generate initial config file. - Removed tracking of config.yml for security. - Updated README for install process and new delete feature. --- .gitignore | 4 + README.md | 70 ++- custom_formats/Custom Formats (Radarr).json | 582 +++++++++++++++++++- custom_formats/Custom Formats (Sonarr).json | 516 ++++++++++++++++- deletarr.py | 164 ++++++ develop/docker-compose.yml | 23 + config.yml => setup.py | 5 + 7 files changed, 1329 insertions(+), 35 deletions(-) create mode 100644 deletarr.py create mode 100644 develop/docker-compose.yml rename config.yml => setup.py (82%) diff --git a/.gitignore b/.gitignore index 68bc17f..25380b1 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,7 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +config.yml + +exports/ \ No newline at end of file diff --git a/README.md b/README.md index 9a6e8e6..9643636 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,13 @@ Profilarr is a Python-based tool designed to add import/export/sync functionalit 1. Download the latest Profilarr package from the release section. 2. Extract its contents into a folder. -3. Open the `config.yml` file in a text editor. +3. Run `python setup.py` in your command line interface to generate a config file. + - This will create a `config.yml` file in the same directory as `setup.py`. +4. Open the `config.yml` file in a text editor. - Add the URL and API key to the master instances of Radarr / Sonarr. - If syncing, add the URL, API key and a name to each extra instance of Radarr / Sonarr. - If exporting, adjust the `export_path` to your desired export location. -4. Save the changes. +5. Save the changes. ## 🚀 Usage @@ -250,6 +252,66 @@ Deleted temporary directory: ./temp_directory PS Z:\Profilarr> ``` +### Deleting + +1. Run `python deletarr.py` in your command line interface. +2. Select the instance from which you wish to delete data. +3. Choose between deleting Custom Formats or Quality Profiles. +4. Select specific items by typing their numbers separated by commas, or type 'all' to delete everything. + +#### Example: Deleting Custom Formats + +```plaintext +PS Z:\Profilarr> python deletarr.py + +Available instances to delete from: +1. Sonarr [Master] +2. Radarr [Master] +Enter the number of the instance to delete from: 2 + +Choose what to delete: +1. Custom Formats +2. Quality Profiles +Enter your choice (1/2): 1 + +Deleting selected custom formats... + +Available items: +1. UHDBits +2. Dolby Vision w/out Fallback +... +132. h265 (4k) +133. MAX +Your choice: all + +Deleting custom format 'UHDBits': SUCCESS +... +Deleting custom format 'MAX': SUCCESS +``` + +#### Example: Deleting Quality Profiles + +```plaintext +PS Z:\Profilarr> python deletarr.py + +Choose what to delete: +1. Custom Formats +2. Quality Profiles +Enter your choice (1/2): 2 + +Deleting selected quality profiles... + +Available items: +1. 1080p Balanced +... +11. 2160p Optimal +Your choice: all + +Deleting quality profile '1080p Balanced': SUCCESS +... +Deleting quality profile '2160p Optimal': SUCCESS +``` + ### Radarr and Sonarr Compatibility - You are only able to import / sync files to the app that is included in the file name (e.g. `Radarr` or `Sonarr`). @@ -264,6 +326,10 @@ PS Z:\Profilarr> - **User Interface (UI):** Development of a graphical user interface (GUI) for easier and more intuitive interaction with Profilarr. This UI will cater to users who prefer graphical over command-line interactions. - **Automatic Updates:** Implement an auto-update mechanism for Profilarr, ensuring users always have access to the latest features, improvements, and bug fixes without manual intervention. +## Contributing + +- I've added a docker compose file for testing custom formats / quality profiles. Run `docker-compose up -d` to start the Radarr/ Sonarr test containers. Add your API keys to the `config.yml` file and begin testing! + # TRaSH Guides Some custom formats found here have been interated on from the trash guides. Credit for these goes entirely to trash, and can be found on their site here. It is not my intention to steal their work, but rather to build on it and make it more accessible to the average user through my quality profiles. Please check out their site for more information on their work. diff --git a/custom_formats/Custom Formats (Radarr).json b/custom_formats/Custom Formats (Radarr).json index cf5235e..a008c2a 100644 --- a/custom_formats/Custom Formats (Radarr).json +++ b/custom_formats/Custom Formats (Radarr).json @@ -8928,6 +8928,27 @@ "isFloat": false } ] + }, + { + "name": "ETHEL", + "implementation": "ReleaseGroupSpecification", + "implementationName": "Release Group", + "infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2", + "negate": false, + "required": false, + "fields": [ + { + "order": 0, + "name": "value", + "label": "Regular Expression", + "helpText": "Custom Format RegEx is Case Insensitive", + "value": "(?<=^|[\\s.-])ETHEL\\b", + "type": "textbox", + "advanced": false, + "privacy": "normal", + "isFloat": false + } + ] } ] }, @@ -9272,7 +9293,7 @@ "name": "value", "label": "Regular Expression", "helpText": "Custom Format RegEx is Case Insensitive", - "value": "\\b(FraMeSToR|HQMUX|SiCFoI|playBD|RYU|ElNeekster|CiNEPHiLES|3L|EDV|Kenobi|TRiToN|HDH|NTb)\\b", + "value": "\\b(FraMeSToR|HQMUX|SiCFoI|playBD|RYU|ElNeekster|CiNEPHiLES|3L|EDV|Kenobi|TRiToN|HDH|NTb|Flights)\\b", "type": "textbox", "advanced": false, "privacy": "normal", @@ -11896,7 +11917,7 @@ "name": "value", "label": "Regular Expression", "helpText": "Custom Format RegEx is Case Insensitive", - "value": "^(?!.*(?i:remux)).*([hH]\\s*\\.?\\s*265)", + "value": "(?i)h\\s*\\.?\\s*265", "type": "textbox", "advanced": false, "privacy": "normal", @@ -11938,7 +11959,7 @@ "name": "value", "label": "Regular Expression", "helpText": "Custom Format RegEx is Case Insensitive", - "value": "(?i)(REMUX|DVDRip)", + "value": "Remux", "type": "textbox", "advanced": false, "privacy": "normal", @@ -11948,8 +11969,8 @@ }, { "name": "WEB", - "implementation": "ReleaseTitleSpecification", - "implementationName": "Release Title", + "implementation": "SourceSpecification", + "implementationName": "Source", "infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2", "negate": false, "required": true, @@ -11957,11 +11978,142 @@ { "order": 0, "name": "value", - "label": "Regular Expression", - "helpText": "Custom Format RegEx is Case Insensitive", - "value": "(? len(sources): + print_error("Invalid input. Please enter a valid number.") + choice = input(Colors.HEADER + "Enter the number of the instance to delete from: " + Colors.ENDC).strip() + + selected_app, selected_name = sources[int(choice) - 1] + print() + return selected_app, selected_name + +def user_select_items_to_delete(items): + print(Colors.HEADER + "\nAvailable items:" + Colors.ENDC) + for idx, item in enumerate(items, start=1): + print(f"{idx}. {item['name']}") + print(Colors.HEADER + "Type the number(s) of the items you wish to delete separated by commas, or type 'all' to delete everything." + Colors.ENDC) + + selection = input(Colors.HEADER + "Your choice: " + Colors.ENDC).strip().lower() + if selection == 'all': + return [item['id'] for item in items] # Return all IDs if "all" is selected + else: + selected_ids = [] + try: + selected_indices = [int(i) - 1 for i in selection.split(',') if i.isdigit()] + for idx in selected_indices: + if idx < len(items): + selected_ids.append(items[idx]['id']) + return selected_ids + except ValueError: + print_error("Invalid input. Please enter a valid number or 'all'.") + return [] + +def delete_custom_formats(source_config): + print(Colors.OKBLUE + "\nDeleting selected custom formats..." + Colors.ENDC) + headers = {"X-Api-Key": source_config['api_key']} + get_url = f"{source_config['base_url']}/api/v3/customformat" + + try: + response = requests.get(get_url, headers=headers) + if response.status_code == 200: + formats_to_delete = response.json() + selected_ids = user_select_items_to_delete(formats_to_delete) + + for format_id in selected_ids: + delete_url = f"{get_url}/{format_id}" + del_response = requests.delete(delete_url, headers=headers) + format_name = next((item['name'] for item in formats_to_delete if item['id'] == format_id), "Unknown") + if del_response.status_code in [200, 202, 204]: + print(Colors.OKBLUE + f"Deleting custom format '{format_name}': " + Colors.ENDC + Colors.OKGREEN + "SUCCESS" + Colors.ENDC) + else: + print(Colors.OKBLUE + f"Deleting custom format '{format_name}': " + Colors.ENDC + Colors.FAIL + "FAIL" + Colors.ENDC) + else: + print_error("Failed to retrieve custom formats for deletion!") + except requests.exceptions.ConnectionError: + print_connection_error() + +def delete_quality_profiles(source_config): + print(Colors.OKBLUE + "\nDeleting selected quality profiles..." + Colors.ENDC) + headers = {"X-Api-Key": source_config['api_key']} + get_url = f"{source_config['base_url']}/api/v3/qualityprofile" + + try: + response = requests.get(get_url, headers=headers) + if response.status_code == 200: + profiles_to_delete = response.json() + selected_ids = user_select_items_to_delete(profiles_to_delete) + + for profile_id in selected_ids: + delete_url = f"{get_url}/{profile_id}" + del_response = requests.delete(delete_url, headers=headers) + profile_name = next((item['name'] for item in profiles_to_delete if item['id'] == profile_id), "Unknown") + if del_response.status_code in [200, 202, 204]: + print(Colors.OKBLUE + f"Deleting quality profile '{profile_name}': " + Colors.ENDC + Colors.OKGREEN + "SUCCESS" + Colors.ENDC) + else: + # Handle failure due to the profile being in use or other errors + error_message = "Failed to delete due to an unknown error." + try: + # Attempt to parse JSON error message from response + error_details = del_response.json() + if 'message' in error_details: + error_message = error_details['message'] + elif 'error' in error_details: + error_message = error_details['error'] + except json.JSONDecodeError: + # If response is not JSON or doesn't have expected fields + error_message = del_response.text or "Failed to delete with no detailed error message." + + print(Colors.OKBLUE + f"Deleting quality profile '{profile_name}': " + Colors.ENDC + Colors.FAIL + f"FAIL - {error_message}" + Colors.ENDC) + else: + print_error("Failed to retrieve quality profiles for deletion!") + except requests.exceptions.ConnectionError: + print_connection_error() + +def get_app_config(app_name, instance_name): + if instance_name.endswith("[Master]"): + return master_config[app_name] + else: + instance_name = instance_name.replace(f"{app_name.capitalize()} [", "").replace("]", "") + extras = config['instances']['extras'].get(app_name, []) + for instance in extras: + if instance['name'] == instance_name: + return instance + raise ValueError(f"Configuration for {app_name} - {instance_name} not found.") + +if __name__ == "__main__": + selected_app, selected_instance = get_user_choice() + source_config = get_app_config(selected_app, selected_instance) + source_config['app_name'] = selected_app + + print(Colors.HEADER + "\nChoose what to delete:" + Colors.ENDC) + print("1. Custom Formats") + print("2. Quality Profiles") + choice = input(Colors.HEADER + "Enter your choice (1/2): " + Colors.ENDC).strip() + + if choice == "1": + delete_custom_formats(source_config) + elif choice == "2": + delete_quality_profiles(source_config) diff --git a/develop/docker-compose.yml b/develop/docker-compose.yml new file mode 100644 index 0000000..d7e3c05 --- /dev/null +++ b/develop/docker-compose.yml @@ -0,0 +1,23 @@ +version: "3.3" +services: + radarr: + image: linuxserver/radarr + container_name: radarr + environment: + - PUID=1000 # user id, change as necessary + - PGID=1000 # group id, change as necessary + - TZ=Europe/London # timezone, change as necessary + ports: + - "7887:7878" # change the left value to the desired host port for Radarr + restart: unless-stopped + + sonarr: + image: linuxserver/sonarr + container_name: sonarr + environment: + - PUID=1000 # user id, change as necessary + - PGID=1000 # group id, change as necessary + - TZ=Europe/London # timezone, change as necessary + ports: + - "8998:8989" # change the left value to the desired host port for Sonarr + restart: unless-stopped diff --git a/config.yml b/setup.py similarity index 82% rename from config.yml rename to setup.py index 81e9cbf..d44e187 100644 --- a/config.yml +++ b/setup.py @@ -1,3 +1,4 @@ +config_content = """ instances: master: sonarr: @@ -18,3 +19,7 @@ instances: settings: export_path: "./exports" +""" + +with open('config.yml', 'w') as file: + file.write(config_content)