From ff79de7724f3fc096767be44ac19206fea5ab080 Mon Sep 17 00:00:00 2001 From: santiagosayshey Date: Mon, 5 Feb 2024 17:20:59 +1030 Subject: [PATCH] v0.3 (#29) * added AMiABLE and PiGNUS to scene groups (#25) * Major Code Refactor (#27) Description Reimplemented Profilarr from the ground up to be more reusable, in addition to implemented a few improvements. Works almost identically to before, but will be much easier to develop for going forward. Improvements Implements feature mentioned in Issue #11. - Custom Formats are now automatically imported before quality profiles are imported. * fixed 2160 remux bug (#28) Fixed bug that was incorrectly prioritising WEBs in 2160p optimal profiles. --- README.md | 391 +++++++--------- deletarr.py | 249 ++++------- develop/docker-compose.yml | 34 +- exportarr.py | 290 +++++------- helpers.py | 131 ++++++ importarr.py | 422 ++++++++---------- .../radarr}/Custom Formats (Radarr).json | 151 +++++++ .../sonarr}/Custom Formats (Sonarr).json | 0 .../radarr}/1080p Balanced (Radarr).json | 15 + ...1080p Balanced (Single Grab) (Radarr).json | 15 + .../radarr}/1080p Optimal (Radarr).json | 15 + .../1080p Optimal (Single Grab) (Radarr).json | 15 + ...0p Transparent (Double Grab) (Radarr).json | 15 + .../radarr}/1080p Transparent (Radarr).json | 15 + ...0p Transparent (Single Grab) (Radarr).json | 15 + .../radarr}/1080p h265 Balanced (Radarr).json | 15 + ... h265 Balanced (Single Grab) (Radarr).json | 15 + .../radarr}/2160p Optimal (Radarr).json | 42 +- .../2160p Optimal (Single Grab) (Radarr).json | 42 +- ...1080p Balanced (Single Grab) (Sonarr).json | 0 .../sonarr}/1080p Balanced (Sonarr).json | 0 .../1080p Optimal (Single Grab) (Sonarr).json | 0 .../sonarr}/1080p Optimal (Sonarr).json | 0 ...0p Transparent (Double Grab) (Sonarr).json | 0 ...0p Transparent (Single Grab) (Sonarr).json | 0 .../sonarr}/1080p Transparent (Sonarr).json | 0 .../1080p h265 Balanced (Sonarr).json | 0 ... h265 Balanced (Single Grab) (Sonarr).json | 0 .../2160p Optimal (Single Grab) (Sonarr).json | 17 +- .../sonarr}/2160p Optimal (Sonarr).json | 17 +- requirements.txt | Bin 254 -> 31 bytes setup.py | 27 +- syncarr.py | 65 +-- 33 files changed, 1151 insertions(+), 862 deletions(-) create mode 100644 helpers.py rename {custom_formats => imports/custom_formats/radarr}/Custom Formats (Radarr).json (98%) rename {custom_formats => imports/custom_formats/sonarr}/Custom Formats (Sonarr).json (100%) rename {profiles => imports/quality_profiles/radarr}/1080p Balanced (Radarr).json (98%) rename {profiles => imports/quality_profiles/radarr}/1080p Balanced (Single Grab) (Radarr).json (98%) rename {profiles => imports/quality_profiles/radarr}/1080p Optimal (Radarr).json (98%) rename {profiles => imports/quality_profiles/radarr}/1080p Optimal (Single Grab) (Radarr).json (98%) rename {profiles => imports/quality_profiles/radarr}/1080p Transparent (Double Grab) (Radarr).json (98%) rename {profiles => imports/quality_profiles/radarr}/1080p Transparent (Radarr).json (98%) rename {profiles => imports/quality_profiles/radarr}/1080p Transparent (Single Grab) (Radarr).json (98%) rename {profiles => imports/quality_profiles/radarr}/1080p h265 Balanced (Radarr).json (98%) rename {profiles => imports/quality_profiles/radarr}/1080p h265 Balanced (Single Grab) (Radarr).json (98%) rename {profiles => imports/quality_profiles/radarr}/2160p Optimal (Radarr).json (96%) rename {profiles => imports/quality_profiles/radarr}/2160p Optimal (Single Grab) (Radarr).json (96%) rename {profiles => imports/quality_profiles/sonarr}/1080p Balanced (Single Grab) (Sonarr).json (100%) rename {profiles => imports/quality_profiles/sonarr}/1080p Balanced (Sonarr).json (100%) rename {profiles => imports/quality_profiles/sonarr}/1080p Optimal (Single Grab) (Sonarr).json (100%) rename {profiles => imports/quality_profiles/sonarr}/1080p Optimal (Sonarr).json (100%) rename {profiles => imports/quality_profiles/sonarr}/1080p Transparent (Double Grab) (Sonarr).json (100%) rename {profiles => imports/quality_profiles/sonarr}/1080p Transparent (Single Grab) (Sonarr).json (100%) rename {profiles => imports/quality_profiles/sonarr}/1080p Transparent (Sonarr).json (100%) rename {profiles => imports/quality_profiles/sonarr}/1080p h265 Balanced (Sonarr).json (100%) rename {profiles => imports/quality_profiles/sonarr}/1080p h265 Balanced (Single Grab) (Sonarr).json (100%) rename {profiles => imports/quality_profiles/sonarr}/2160p Optimal (Single Grab) (Sonarr).json (98%) rename {profiles => imports/quality_profiles/sonarr}/2160p Optimal (Sonarr).json (98%) diff --git a/README.md b/README.md index 9643636..00a21ef 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ Profilarr is a Python-based tool designed to add import/export/sync functionalit ## ⚠️ Before Continuing - **This tool will overwrite any custom formats in your \*arr installation that have the same name.** -- **Custom Formats MUST be imported before syncing any premade profile.** - **Always back up your Radarr and Sonarr configurations before using Profilarr to avoid unintended data loss.** (Seriously, do it. Even I've lost data to this tool because I forgot to back up my configs.) ## 🛠️ Installation @@ -29,73 +28,47 @@ Profilarr is a Python-based tool designed to add import/export/sync functionalit - 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. + - If importing non Dictionarry files, adjust the `import_path` to your desired import location. 5. Save the changes. ## 🚀 Usage ### Importing -1. Run `python importarr.py` in your command line interface. -2. Follow the on-screen prompts to select the app and the data you want to import. -3. Choose the specific file for Custom Formats or select a profile for Quality Profiles. -4. The data will be imported to your selected Radarr or Sonarr installation. +Note: For users who start using Profilarr before v0.3, you no longer need to manually import custom formats. They will be imported automatically. Quality Profiles still require manual selection. -#### Custom Format Import Example - -```bash -PS Z:\Profilarr> py importarr.py -Available instances to import to: -1. Sonarr [Master] -2. Radarr [Master] -3. Sonarr [4k-sonarr] -4. Radarr [4k-radarr] -Enter the number of the instance to import to: 4 +1. If importing Dictionarry files, make sure the import path is `./imports` (This is the default path). +2. If importing non Dictionarry files, make sure the import path is set to your desired import location. +3. Run `python importarr.py` in your command line interface. +4. Follow the on-screen prompts to select your desired app and which instance(s) to import to. +5. Choose your desired quality profile(s) to import. +#### Example: Importing 1080p Transparent and 2160p Optimal Quality Profiles -Choose what to import: -1. Custom Formats -2. Quality Profiles -Enter your choice (1/2): 1 - -Available files: -1. Custom Formats (Radarr).json -Select a file to import (or 'all' for all files): 1 - -Adding custom format 'D-Z0N3': SUCCESS -Adding custom format 'DON': SUCCESS -Adding custom format 'EbP': SUCCESS -Adding custom format 'Geek': SUCCESS -Adding custom format 'TayTo': SUCCESS -Adding custom format 'ZQ': SUCCESS -Adding custom format 'VietHD': SUCCESS -Adding custom format 'CtrlHD': SUCCESS -Adding custom format 'HiFi': SUCCESS -Adding custom format 'FoRM': SUCCESS -Adding custom format 'HiDt': SUCCESS -Adding custom format 'SA89': SUCCESS -... - -Successfully added 0 custom formats, updated 131 custom formats. ``` - -#### Quality Profile Import Example - -```bash -PS Z:\Profilarr> py importarr.py -Available instances to import to: -1. Sonarr [Master] -2. Radarr [Master] -3. Sonarr [4k-sonarr] -4. Radarr [4k-radarr] -Enter the number of the instance to import to: 4 - - -Choose what to import: -1. Custom Formats -2. Quality Profiles -Enter your choice (1/2): 2 - -Available files: +Select your app of choice +1. Radarr +2. Sonarr +Enter your choice: +1 +Select your Radarr instance +1. Radarr (Master) +2. Radarr (4k-radarr) +Choose an instance by number, multiple numbers separated by commas or type 'all' for all instances: +2 + +Importing custom formats to Radarr : 4k-radarr + +Adding custom format 'D-Z0N3' : SUCCESS +Adding custom format 'DON' : SUCCESS +Adding custom format 'EbP' : SUCCESS +Adding custom format 'Geek' : SUCCESS +Adding custom format 'TayTo' : SUCCESS +... and 129 more. + +Successfully added 0 custom formats, updated 134 custom formats. + +Available profiles: 1. 1080p Balanced (Radarr).json 2. 1080p Balanced (Single Grab) (Radarr).json 3. 1080p h265 Balanced (Radarr).json @@ -107,209 +80,177 @@ Available files: 9. 1080p Transparent (Single Grab) (Radarr).json 10. 2160p Optimal (Radarr).json 11. 2160p Optimal (Single Grab) (Radarr).json -Select a file to import (or 'all' for all files): all - -Successfully added Quality Profile 1080p Balanced -Successfully added Quality Profile 1080p Balanced (Single Grab) -Successfully added Quality Profile 1080p h265 Balanced -Successfully added Quality Profile 1080p h265 Balanced (Single Grab) -Successfully added Quality Profile 1080p Optimal -Successfully added Quality Profile 1080p Optimal (Single Grab) -Successfully added Quality Profile 1080p Transparent (Double Grab) -Successfully added Quality Profile 1080p Transparent -Successfully added Quality Profile 1080p Transparent (Single Grab) -Successfully added Quality Profile 2160p Optimal -Successfully added Quality Profile 2160p Optimal (Single Grab) -PS Z:\Profilarr> + +Enter the numbers of the profiles you want to import separated by commas, or type 'all' to import all profiles: +8,10 +Importing Quality Profiles to Radarr : 4k-radarr + +Adding '1080p Transparent' quality profile : SUCCESS +Adding '2160p Optimal' quality profile : SUCCESS ``` ### Exporting -1. Run `python exportarr.py` in your command line interface. -2. Choose the instance you want to export from. -3. Choose the data you want to export. -4. The data will be exported to `exports/{instance_type}/{instance_name}/{data_type}`. +1. Make sure the export path is set to your desired export location. The default is `./exports`. +2. Run `python exportarr.py` in your command line interface. +3. Follow the on-screen prompts to select your desired app and which instance(s) to export from. +4. Choose the data you want to export. +5. The data will be exported to `exports/{data_type}/{app}/`. #### Example ```bash -PS Z:\Profilarr> py exportarr.py -Available sources to export from: -1. Sonarr [Master] -2. Radarr [Master] -3. Sonarr [4k-sonarr] -4. Radarr [4k-radarr] -Enter the number of the app to export from: 2 - -Choose what to export: -1. Custom Formats -2. Quality Profiles -3. Both -Enter your choice (1/2/3): 3 - -Attempting to access Radarr at http://localhost:7878 -Found 131 custom formats. - - D-Z0N3 - - DON - - EbP - - Geek - - TayTo - - ZQ - - VietHD - - CtrlHD - - HiFi - - FoRM -... and 121 more. -Saved to './exports\radarr\master\custom_formats\Custom Formats (Radarr).json' - -Attempting to access Radarr at http://localhost:7878 -Found 13 quality profiles. - - 1080p Optimal - - 2160p Optimal - - 1080p Balanced - - 1080p Transparent - - 1080p Transparent (Double Grab) - - 1080p Transparent (Single Grab) - - 1080p Balanced (Single Grab) - - 1080p h265 Balanced - - 1080p h265 Balanced (Single Grab) - - 1080p x265 HDR Transparent -... and 3 more. -Saved to 'exports\radarr\master\profiles' - -PS Z:\Profilarr> +Select your app of choice +1. Radarr +2. Sonarr +Enter your choice: +1 +Select your Radarr instance +1. Radarr (Master) +2. Radarr (4k-radarr) +Choose an instance by number, multiple numbers separated by commas or type 'all' for all instances: +2 + +Exporting Custom Formats for Radarr : 4k-radarr +Exported 134 custom formats to ./exports/custom_formats/Radarr for 4k-radarr + +Exporting Quality Profiles for Radarr : 4k-radarr... +Exported 2 quality profiles to ./exports/quality_profiles/Radarr for 4k-radarr ``` ### Syncing +1. Make sure the import path is set to whatever your export path is. This is important, as the script will look for the exported files in this location. 1. Run `python syncarr.py` in your command line interface. -2. The script will automatically export data from the master instance and import it to all other instances specified in `config.json`. -3. This feature is designed to manage multiple Radarr/Sonarr instances, syncing profiles and formats seamlessly. +1. The script will automatically export data from the master instance and import it to all other instances specified in `config.json`. #### Example ```bash PS Z:\Profilarr> py syncarr.py -Select the app you want to sync: +Select your app of choice 1. Radarr 2. Sonarr -Enter your choice (1 or 2): 2 -Attempting to access Sonarr at http://localhost:8989 -Found 135 custom formats. - - D-Z0N3 - - DON - - EbP - - Geek - - TayTo - - ZQ - - VietHD - - CtrlHD - - HiFi - - FoRM -... and 125 more. -Saved to './temp_directory/custom_formats\Custom Formats (Sonarr).json' - -Attempting to access Sonarr at http://localhost:8989 -Found 11 quality profiles. - - 1080p Transparent - - 2160p Optimal - - 1080p Transparent (Single Grab) - - 1080p Transparent (Double Grab) - - 1080p Balanced - - 1080p Balanced (Single Grab) - - 1080p h265 Balanced - - 1080p h265 Balanced (Single Grab) - - 1080p Optimal - - 1080p Optimal (Single Grab) -... and 1 more. -Saved to 'temp_directory\quality_profiles' - -Importing to instance: 4k-sonarr -Adding custom format 'D-Z0N3': SUCCESS -Adding custom format 'DON': SUCCESS -Adding custom format 'EbP': SUCCESS -Adding custom format 'Geek': SUCCESS -Adding custom format 'TayTo': SUCCESS -Adding custom format 'ZQ': SUCCESS -Adding custom format 'VietHD': SUCCESS -Adding custom format 'CtrlHD': SUCCESS -Adding custom format 'HiFi': SUCCESS -... and 125 more. - -Successfully added 135 custom formats, updated 0 custom formats. -Successfully added Quality Profile 1080p Balanced (Single Grab) -Successfully added Quality Profile 1080p Balanced -Successfully added Quality Profile 1080p h265 Balanced -Successfully added Quality Profile 1080p h265 Balanced (Single Grab) -Successfully added Quality Profile 1080p Optimal (Single Grab) -Successfully added Quality Profile 1080p Optimal -Successfully added Quality Profile 1080p Transparent (Double Grab) -Successfully added Quality Profile 1080p Transparent (Single Grab) -Successfully added Quality Profile 1080p Transparent -Successfully added Quality Profile 2160p Optimal (Single Grab) -Successfully added Quality Profile 2160p Optimal -Deleted temporary directory: ./temp_directory -PS Z:\Profilarr> -``` - -### Deleting +Enter your choice: +1 +Exporting Custom Formats for radarr : Master +Exported 134 custom formats to ./exports\custom_formats\radarr for Master -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. +Exporting Quality Profiles for radarr : Master... +Exported 14 quality profiles to ./exports\quality_profiles\radarr for Master -#### Example: Deleting Custom Formats +Importing custom formats to radarr : 4k-radarr -```plaintext -PS Z:\Profilarr> python deletarr.py +... +Updating custom format 'Blu-Ray (Remux)' : SUCCESS +Updating custom format 'MAX' : SUCCESS +Updating custom format 'h265 (4k)' : SUCCESS +Updating custom format 'TEST FLAC' : SUCCESS -Available instances to delete from: -1. Sonarr [Master] -2. Radarr [Master] -Enter the number of the instance to delete from: 2 +Successfully added 134 custom formats, updated 0 custom formats. -Choose what to delete: -1. Custom Formats -2. Quality Profiles -Enter your choice (1/2): 1 +Available profiles: +1. 1080p Balanced (Radarr).json +2. 1080p Balanced (Single Grab) (Radarr).json +3. 1080p h265 Balanced (Radarr).json +4. 1080p h265 Balanced (Single Grab) (Radarr).json +5. 1080p Optimal (Radarr).json +6. 1080p Optimal (Single Grab) (Radarr).json +7. 1080p Transparent (Double Grab) (Radarr).json +8. 1080p Transparent (Radarr).json +9. 1080p Transparent (Single Grab) (Radarr).json +10. 2160p Optimal (Radarr).json +11. 2160p Optimal (Single Grab) (Radarr).json -Deleting selected custom formats... +Enter the numbers of the profiles you want to import separated by commas, or type 'all' to import all profiles: +all +Importing Quality Profiles to radarr : 4k-radarr + +Adding '1080p Balanced' quality profile : SUCCESS +Adding '1080p Balanced (Single Grab)' quality profile : SUCCESS +Adding '1080p h265 Balanced' quality profile : SUCCESS +Adding '1080p h265 Balanced (Single Grab)' quality profile : SUCCESS +Adding '1080p Optimal' quality profile : SUCCESS +Adding '1080p Optimal (Single Grab)' quality profile : SUCCESS +Adding '1080p Transparent (Double Grab)' quality profile : SUCCESS +Updating '1080p Transparent' quality profile : SUCCESS +Adding '1080p Transparent (Single Grab)' quality profile : SUCCESS +Updating '2160p Optimal' quality profile : SUCCESS +Adding '2160p Optimal (Single Grab)' quality profile : SUCCESS +``` -Available items: -1. UHDBits -2. Dolby Vision w/out Fallback -... -132. h265 (4k) -133. MAX -Your choice: all +### Deleting -Deleting custom format 'UHDBits': SUCCESS -... -Deleting custom format 'MAX': SUCCESS -``` +1. Run `python deletarr.py` in your command line interface. +2. Select the instance(s) from which you wish to delete data. +3. Choose between deleting Custom Formats, Quality Profiles or both +4. Select specific items by typing their numbers separated by commas, or type 'all' to delete everything. -#### Example: Deleting Quality Profiles +#### Example ```plaintext -PS Z:\Profilarr> python deletarr.py - -Choose what to delete: +Select your app of choice +1. Radarr +2. Sonarr +Enter your choice: +1 +Select your Radarr instance +1. Radarr (Master) +2. Radarr (4k-radarr) +Choose an instance by number, multiple numbers separated by commas or type 'all' for all instances: +2 + +Please select what you want to delete: 1. Custom Formats 2. Quality Profiles -Enter your choice (1/2): 2 - -Deleting selected quality profiles... - -Available items: -1. 1080p Balanced +3. Both +Enter your choice: 3 +Available items to delete: +1. D-Z0N3 +2. DON +3. EbP +4. Geek +5. TayTo +6. ZQ ... -11. 2160p Optimal + +Enter the number(s) of the items you wish to delete, separated by commas, or type 'all' for all: +Your choice: all +Deleting Custom Format (D-Z0N3) : SUCCESS +Deleting Custom Format (DON) : SUCCESS +Deleting Custom Format (EbP) : SUCCESS +Deleting Custom Format (Geek) : SUCCESS +Deleting Custom Format (TayTo) : SUCCESS +Deleting Custom Format (ZQ) : SUCCESS + +Available items to delete: +1. 1080p Transparent +2. 2160p Optimal +3. 1080p Balanced +4. 1080p Balanced (Single Grab) +5. 1080p h265 Balanced +6. 1080p h265 Balanced (Single Grab) +7. 1080p Optimal +8. 1080p Optimal (Single Grab) +9. 1080p Transparent (Double Grab) +10. 1080p Transparent (Single Grab) +11. 2160p Optimal (Single Grab) + +Enter the number(s) of the items you wish to delete, separated by commas, or type 'all' for all: Your choice: all -Deleting quality profile '1080p Balanced': SUCCESS -... -Deleting quality profile '2160p Optimal': SUCCESS +Deleting Quality Profile (1080p Transparent) : SUCCESS +Deleting Quality Profile (2160p Optimal) : SUCCESS +Deleting Quality Profile (1080p Balanced) : SUCCESS +Deleting Quality Profile (1080p Balanced (Single Grab)) : SUCCESS +Deleting Quality Profile (1080p h265 Balanced) : SUCCESS +Deleting Quality Profile (1080p h265 Balanced (Single Grab)) : SUCCESS +Deleting Quality Profile (1080p Optimal) : SUCCESS +Deleting Quality Profile (1080p Optimal (Single Grab)) : SUCCESS +Deleting Quality Profile (1080p Transparent (Double Grab)) : SUCCESS +Deleting Quality Profile (1080p Transparent (Single Grab)) : SUCCESS +Deleting Quality Profile (2160p Optimal (Single Grab)) : SUCCESS +PS Z:\Profilarr> ``` ### Radarr and Sonarr Compatibility diff --git a/deletarr.py b/deletarr.py index dd67eaf..6052c69 100644 --- a/deletarr.py +++ b/deletarr.py @@ -1,164 +1,101 @@ -import requests -import os -import yaml -import json - -# ANSI escape sequences for colors -class Colors: - HEADER = '\033[95m' # Purple for questions and headers - OKBLUE = '\033[94m' # Blue for actions - OKGREEN = '\033[92m' # Green for success messages - FAIL = '\033[91m' # Red for error messages - ENDC = '\033[0m' # Reset to default - BOLD = '\033[1m' - UNDERLINE = '\033[4m' - -# Load configuration for main app -with open('config.yml', 'r') as config_file: - config = yaml.safe_load(config_file) - master_config = config['instances']['master'] - -def print_success(message): - print(Colors.OKGREEN + message + Colors.ENDC) - -def print_error(message): - print(Colors.FAIL + message + Colors.ENDC) - -def print_connection_error(): - print(Colors.FAIL + "Failed to connect to the service! Please check if it's running and accessible." + Colors.ENDC) - -def get_user_choice(): - print(Colors.HEADER + "\nAvailable instances to delete from:" + Colors.ENDC) - sources = [] - - # Add master installations - for app in master_config: - sources.append((app, f"{app.capitalize()} [Master]")) - - # Add extra installations - if "extras" in config['instances']: - for app, instances in config['instances']['extras'].items(): - for install in instances: - sources.append((app, f"{app.capitalize()} [{install['name']}]")) - - # Display sources with numbers - for idx, (app, name) in enumerate(sources, start=1): - print(f"{idx}. {name}") - - # User selection - choice = input(Colors.HEADER + "Enter the number of the instance to delete from: " + Colors.ENDC).strip() - while not choice.isdigit() or int(choice) < 1 or int(choice) > 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 +from helpers import * 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 + """ + Prompts the user to select items to delete from a given list of items. + Each item in the list is expected to be a dictionary with at least an 'id' and 'name' key. + """ + print_message("Available items to delete:", "purple") + for index, item in enumerate(items, start=1): + print_message(f"{index}. {item['name']}", "green") + + print_message("Enter the number(s) of the items you wish to delete, separated by commas, or type 'all' for all:", "yellow") + user_input = input("Your choice: ").strip().lower() + selected_items = [] + + if user_input == 'all': + return items 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) + indices = user_input.split(',') + for index in indices: + try: + index = int(index.strip()) - 1 + if 0 <= index < len(items): + selected_items.append(items[index]) 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) + print_message("Invalid selection, ignoring.", "red") + except ValueError: + print_message("Invalid input, please enter numbers only.", "red") + + return selected_items + + +def prompt_export_choice(): + """ + Prompt user to choose between exporting Custom Formats, Quality Profiles, or both. + Returns a list of choices. + """ + print_message("Please select what you want to delete:", "blue") + options = {"1": "Custom Formats", "2": "Quality Profiles", "3": "Both"} + for key, value in options.items(): + print_message(f"{key}. {value}", "green") + choice = input("Enter your choice: ").strip() + + # Validate choice + while choice not in options: + print_message("Invalid choice, please select a valid option:", "red") + choice = input("Enter your choice: ").strip() + + if choice == "3": + return ["Custom Formats", "Quality Profiles"] + else: + return [options[choice]] + +def delete_custom_formats_or_profiles(app, instance, item_type, config): + """ + Deletes either custom formats or quality profiles based on the item_type. + """ + api_key = instance['api_key'] + base_url = instance['base_url'] + resource_type = item_type # 'customformat' or 'qualityprofile' + + if item_type == 'customformat': + type = 'Custom Format' + elif item_type == 'qualityprofile': + type = 'Quality Profile' + + # Fetch items to delete + items = make_request('get', base_url, api_key, resource_type) + if items is None or not isinstance(items, list): + return + + # Assuming a function to select items to delete. It could list items and ask the user which to delete. + items_to_delete = user_select_items_to_delete(items) # This needs to be implemented or adapted + + # Proceed to delete selected items + for item in items_to_delete: + item_id = item['id'] + item_name = item['name'] + print_message(f"Deleting {type} ({item_name})", "blue", newline=False) + response = make_request('delete', base_url, api_key, f"{resource_type}/{item_id}") + if response in [200, 202, 204]: + print_message(" : SUCCESS", "green") else: - print_error("Failed to retrieve quality profiles for deletion!") - except requests.exceptions.ConnectionError: - print_connection_error() + print_message(" : FAIL", "red") + +def main(): + app = get_app_choice() + instances = get_instance_choice(app) + config = load_config() + + choices = prompt_export_choice() + for instance in instances: + for choice in choices: + if choice == "Custom Formats": + delete_custom_formats_or_profiles(app, instance, 'customformat', config) + elif choice == "Quality Profiles": + delete_custom_formats_or_profiles(app, instance, 'qualityprofile', config) -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) + main() diff --git a/develop/docker-compose.yml b/develop/docker-compose.yml index d7e3c05..3d50531 100644 --- a/develop/docker-compose.yml +++ b/develop/docker-compose.yml @@ -1,23 +1,37 @@ version: "3.3" + +x-common-settings: &common-settings + environment: + PUID: 1000 # user id, change as necessary + PGID: 1000 # group id, change as necessary + TZ: Europe/London # timezone, change as necessary + restart: unless-stopped + services: radarr: + <<: *common-settings 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 + + radarr2: + <<: *common-settings + image: linuxserver/radarr + container_name: radarr2 + ports: + - "7888:7878" # change the left value to the desired host port for Radarr sonarr: + <<: *common-settings 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 + + sonarr2: + <<: *common-settings + image: linuxserver/sonarr + container_name: sonarr2 + ports: + - "8999:8989" # change the left value to the desired host port for Sonarr diff --git a/exportarr.py b/exportarr.py index debb63a..40ff406 100644 --- a/exportarr.py +++ b/exportarr.py @@ -1,183 +1,133 @@ -import requests +from helpers import * import os import re -import yaml -import json - -# ANSI escape sequences for colors -class Colors: - HEADER = '\033[95m' - OKBLUE = '\033[94m' - OKGREEN = '\033[92m' - WARNING = '\033[93m' - FAIL = '\033[91m' - ENDC = '\033[0m' - BOLD = '\033[1m' - UNDERLINE = '\033[4m' - -# Load configuration for main app -with open('config.yml', 'r') as config_file: - config = yaml.safe_load(config_file) - master_config = config['instances']['master'] - export_base_path = config['settings']['export_path'] - -def get_user_choice(): - sources = [] - print(Colors.HEADER + "Available sources to export from:" + Colors.ENDC) - - # Add master installations - for app in master_config: - sources.append((app, f"{app.capitalize()} [Master]", "master")) - - # Add extra installations - if "extras" in config['instances']: - for app, instances in config['instances']['extras'].items(): - for install in instances: - sources.append((app, f"{app.capitalize()} [{install['name']}]", install['name'])) - - # Display sources with numbers - for idx, (app, name, _) in enumerate(sources, start=1): - print(f"{idx}. {name}") - - # User selection - choice = input("Enter the number of the app to export from: ").strip() - while not choice.isdigit() or int(choice) < 1 or int(choice) > len(sources): - print(Colors.FAIL + "Invalid input. Please enter a valid number." + Colors.ENDC) - choice = input("Enter the number of the app to export from: ").strip() - - selected_app, instance_name = sources[int(choice) - 1][0], sources[int(choice) - 1][2] - print() - return selected_app, instance_name - - -def get_export_choice(): - print(Colors.HEADER + "Choose what to export:" + Colors.ENDC) - print("1. Custom Formats") - print("2. Quality Profiles") - print("3. Both") - choice = input("Enter your choice (1/2/3): ").strip() - while choice not in ["1", "2", "3"]: - print(Colors.FAIL + "Invalid input. Please enter 1, 2, or 3." + Colors.ENDC) - choice = input("Enter your choice (1/2/3): ").strip() - print() - return choice - -def get_app_config(source): - app_config = master_config[source] - return app_config['base_url'], app_config['api_key'] - -def sanitize_filename(filename): - sanitized_filename = re.sub(r'[\\/*?:"<>|]', '_', filename) - return sanitized_filename - -def handle_response_errors(response): - if response.status_code == 401: - print(Colors.FAIL + "Authentication error: Invalid API key." + Colors.ENDC) - elif response.status_code == 403: - print(Colors.FAIL + "Forbidden: Access is denied." + Colors.ENDC) - else: - print(Colors.FAIL + f"An error occurred! (HTTP {response.status_code})" + Colors.ENDC) - print("Response Content: ", response.content.decode('utf-8')) - -def print_saved_items(items, item_type): - if len(items) > 10: - items_to_display = items[:10] - for item in items_to_display: - print(f" - {item}") - print(f"... and {len(items) - 10} more.") + +def prompt_export_choice(): + options = { "1": "Custom Formats", "2": "Quality Profiles" } + + print_message("Please select what you want to export:", "blue") + for number, option in options.items(): + print_message(f"{number}. {option}", "green") + print_message("Enter the number(s) of your choice, multiple separated by commas, or type 'all' for all options", "yellow") + + user_choice = input("Your choice: ") + + if user_choice.lower() == 'all': + return list(options.values()) else: - for item in items: - print(f" - {item}") + return [options[choice] for choice in user_choice.split(',') if choice in options] -def ensure_directory_exists(directory): - if not os.path.exists(directory): - os.makedirs(directory) - print(Colors.OKBLUE + f"Created directory: {directory}" + Colors.ENDC) +def create_export_path(export_path, app): + # Create a directory path for the export + dir_path = os.path.join(export_path, 'custom_formats', app) -def export_cf(source, instance_name, save_path=None): - if save_path is None: - save_path = os.path.join(export_base_path, source, instance_name, 'custom_formats') - ensure_directory_exists(save_path) + # Create the directory if it doesn't exist + os.makedirs(dir_path, exist_ok=True) - base_url, api_key = get_app_config(source) - headers = {"X-Api-Key": api_key} - params = {"apikey": api_key} + return dir_path - print(Colors.OKBLUE + f"Attempting to access {source.capitalize()} at {base_url}" + Colors.ENDC) +def export_custom_formats(app, instances, config): - custom_format_url = f"{base_url}/api/v3/customformat" - try: - response = requests.get(custom_format_url, params=params, headers=headers) + for instance in instances: + print_message(f"Exporting Custom Formats for {app} : {instance['name']}", 'blue') - if response.status_code == 200: - data = response.json() - print(Colors.OKGREEN + f"Found {len(data)} custom formats." + Colors.ENDC) - - saved_formats = [] - for custom_format in data: - custom_format.pop('id', None) - saved_formats.append(custom_format['name']) - - file_path = os.path.join(save_path, f'Custom Formats ({source.capitalize()}).json') - with open(file_path, 'w') as f: - json.dump(data, f, indent=4) - - print_saved_items(saved_formats, "Custom Formats") - print(Colors.OKGREEN + f"Saved to '{file_path}'" + Colors.ENDC) - print() - else: - handle_response_errors(response) - - except requests.exceptions.ConnectionError: - print(Colors.FAIL + f"Failed to connect to {source.capitalize()}! Please check if it's running and accessible." + Colors.ENDC) - - - -def export_qf(source, instance_name, save_path=None): - if save_path is None: - save_path = os.path.join(export_base_path, source, instance_name, 'profiles') - ensure_directory_exists(save_path) - - base_url, api_key = get_app_config(source) - headers = {"X-Api-Key": api_key} - params = {"apikey": api_key} - - print(Colors.OKBLUE + f"Attempting to access {source.capitalize()} at {base_url}" + Colors.ENDC) - - try: - response = requests.get(f"{base_url}/api/v3/qualityprofile", params=params, headers=headers) - - if response.status_code == 200: - quality_profiles = response.json() - print(Colors.OKGREEN + f"Found {len(quality_profiles)} quality profiles." + Colors.ENDC) - - saved_profiles = [] - for profile in quality_profiles: - profile.pop('id', None) - profile_name = profile.get('name', 'unnamed_profile') - profile_name = sanitize_filename(profile_name) - profile_filename = f"{profile_name} ({source.capitalize()}).json" - profile_filepath = os.path.join(save_path, profile_filename) - with open(profile_filepath, 'w') as file: - json.dump([profile], file, indent=4) - saved_profiles.append(profile_name) # Add the profile name to the list - - print_saved_items(saved_profiles, "Quality Profiles") - print(Colors.OKGREEN + f"Saved to '{os.path.normpath(save_path)}'" + Colors.ENDC) # Normalize the path - print() - else: - handle_response_errors(response) - - except requests.exceptions.ConnectionError: - print(Colors.FAIL + f"Failed to connect to {source.capitalize()}! Please check if it's running and accessible." + Colors.ENDC) + url = instance['base_url'] + api_key = instance['api_key'] + # Get the export path from the config + export_path = config['settings']['export_path'] -if __name__ == "__main__": - user_choice, instance_name = get_user_choice() - export_choice = get_export_choice() + # Create the export directory + dir_path = create_export_path(export_path, app) + + # Assuming 'export' is a valid resource_type for the API + response = make_request('get', url, api_key, 'customformat') + + successful_exports = 0 # Counter for successful exports + + # Scrub the JSON data and save each custom format in its own file + all_custom_formats = [] + for custom_format in response: + # Remove the 'id' field + custom_format.pop('id', None) + + all_custom_formats.append(custom_format) + successful_exports += 1 # Increment the counter if the export was successful + + # Hardcode the file name as 'Custom Formats (Radarr).json' + file_name = f"Custom Formats ({app.capitalize()} - {instance['name']}).json" + + # Save all custom formats to a single file in the export directory + try: + with open(os.path.join(dir_path, file_name), 'w') as f: + json.dump(all_custom_formats, f, indent=4) + status = 'SUCCESS' + status_color = 'green' + except Exception as e: + status = 'FAILED' + status_color = 'red' + + print_message(f"Exported {successful_exports} custom formats to {dir_path} for {instance['name']}", 'yellow') + print() + +def create_quality_profiles_export_path(app, config): + # Get the export path from the config + export_path = config['settings']['export_path'] - if export_choice in ["1", "3"]: - export_cf(user_choice, instance_name) - if export_choice in ["2", "3"]: - export_qf(user_choice, instance_name) \ No newline at end of file + # Create a directory path for the export + dir_path = os.path.join(export_path, 'quality_profiles', app) + + # Create the directory if it doesn't exist + os.makedirs(dir_path, exist_ok=True) + + return dir_path + +def export_quality_profiles(app, instances, config): + for instance in instances: + print_message(f"Exporting Quality Profiles for {app} : {instance['name']}...", 'blue') + url = instance['base_url'] + api_key = instance['api_key'] + + # Create the export directory + dir_path = create_quality_profiles_export_path(app, config) + + # Assuming 'qualityprofile' is the valid resource_type for the API + response = make_request('get', url, api_key, 'qualityprofile') + + successful_exports = 0 # Counter for successful exports + + # Scrub the JSON data and save each quality profile in its own file + for quality_profile in response: + # Remove the 'id' field + quality_profile.pop('id', None) + + # Create a file name from the quality profile name and app + file_name = f"{quality_profile['name']} ({app.capitalize()} - {instance['name']}).json" + file_name = re.sub(r'[\\/*?:"<>|]', '', file_name) # Remove invalid characters + + # Save the quality profile to a file in the export directory + try: + with open(os.path.join(dir_path, file_name), 'w') as f: + json.dump([quality_profile], f, indent=4) # Wrap quality_profile in a list + status = 'SUCCESS' + status_color = 'green' + except Exception as e: + status = 'FAILED' + status_color = 'red' + if status == 'SUCCESS': + successful_exports += 1 # Increment the counter if the export was successful + + print_message(f"Exported {successful_exports} quality profiles to {dir_path} for {instance['name']}", 'yellow') + print() + +def main(): + app = get_app_choice() + instances = get_instance_choice(app) + config = load_config() + + export_custom_formats(app, instances, config) + export_quality_profiles(app, instances, config) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/helpers.py b/helpers.py new file mode 100644 index 0000000..d884db1 --- /dev/null +++ b/helpers.py @@ -0,0 +1,131 @@ +import yaml +import json +import requests +from requests.exceptions import ConnectionError, Timeout, TooManyRedirects +import json + +class Colors: + GREEN = '\033[92m' + RED = '\033[91m' + YELLOW = '\033[93m' + BLUE = '\033[94m' + PURPLE = '\033[95m' + ENDC = '\033[0m' + +class Apps: + APP_CHOICES = { + "1": "Radarr", + "2": "Sonarr", + # Add more apps here as needed + } + +def print_message(message, message_type='', newline=True): + color = Colors.ENDC # default color + message_type = message_type.lower() + + if message_type == 'green': + color = Colors.GREEN + elif message_type == 'red': + color = Colors.RED + elif message_type == 'yellow': + color = Colors.YELLOW + elif message_type == 'blue': + color = Colors.BLUE + elif message_type == 'purple': + color = Colors.PURPLE + + if newline: + print(color + message + Colors.ENDC) + else: + print(color + message + Colors.ENDC, end='') + + +def load_config(): + with open('config.yml', 'r') as config_file: + config = yaml.safe_load(config_file) + return config + +def get_app_choice(): + print_message("Select your app of choice", "blue") + # Dynamically generate the app selection menu + app_menu = "\n".join([f"{key}. {value}" for key, value in Apps.APP_CHOICES.items()]) + print_message(app_menu) + print_message("Enter your choice: ", "blue") + app_choice = input().strip() + + while app_choice not in Apps.APP_CHOICES.keys(): + print_message("Invalid input. Please enter a valid choice.", "red") + app_choice = input().strip() + + app = Apps.APP_CHOICES[app_choice] + + return app + +def get_instance_choice(app): + config = load_config() + app_instances = config['instances'].get(app.lower(), []) + + print_message(f"Select your {app.capitalize()} instance", "blue") + # Display instances and prompt for choice + for i, instance in enumerate(app_instances, start=1): + print_message(f"{i}. {app.capitalize()} ({instance['name']})") + + print_message("Choose an instance by number, multiple numbers separated by commas or type 'all' for all instances: ", "blue") + choice = input().strip() + print() + selected_instances = [] + + if choice.lower() == 'all': + selected_instances = app_instances + else: + choices = choice.split(',') + for choice in choices: + choice = choice.strip() # remove any leading/trailing whitespace + while not choice.isdigit() or int(choice) < 1 or int(choice) > len(app_instances): + print_message("Invalid input. Please select a valid number.", "warning") + choice = input().strip() + selected_instance = app_instances[int(choice) - 1] + selected_instances.append(selected_instance) + + return selected_instances + +def make_request(request_type, url, api_key, resource_type, json_payload=None): + full_url = f"{url}/api/v3/{resource_type}" + + headers = {"X-Api-Key": api_key} + + try: + # Make the appropriate request based on the request_type + if request_type.lower() == 'get': + response = requests.get(full_url, headers=headers, json=json_payload) + elif request_type.lower() == 'post': + response = requests.post(full_url, headers=headers, json=json_payload) + elif request_type.lower() == 'put': + response = requests.put(full_url, headers=headers, json=json_payload) + elif request_type.lower() == 'delete': + response = requests.delete(full_url, headers=headers) + return response.status_code + elif request_type.lower() == 'patch': + response = requests.patch(full_url, headers=headers, json=json_payload) + else: + raise ValueError("Unsupported request type provided.") + + # Process response + if response.status_code in [200, 201, 202]: + try: + return response.json() + except json.JSONDecodeError: + print_message("Failed to decode JSON response.", "red") + return None + elif response.status_code == 401: + print_message("Unauthorized. Check your API key.", "red") + elif response.status_code == 409: + print_message("Conflict detected. The requested action could not be completed.", "red") + else: + print_message(f"HTTP Error {response.status_code}.", "red") + except (ConnectionError, Timeout, TooManyRedirects) as e: + # Update the message here to suggest checking the application's accessibility + print_message("Network error. Make sure the application is running and accessible.", "red") + + return None + diff --git a/importarr.py b/importarr.py index eca09d4..e36a1cb 100644 --- a/importarr.py +++ b/importarr.py @@ -1,265 +1,211 @@ -import requests +from helpers import * import os -import re -import yaml +import fnmatch import json -# ANSI escape sequences for colors -class Colors: - HEADER = '\033[95m' - OKBLUE = '\033[94m' - OKGREEN = '\033[92m' - WARNING = '\033[93m' - FAIL = '\033[91m' - ENDC = '\033[0m' - BOLD = '\033[1m' - UNDERLINE = '\033[4m' - -# Load configuration for main app -with open('config.yml', 'r') as config_file: - config = yaml.safe_load(config_file) - master_config = config['instances']['master'] - -def print_success(message): - print(Colors.OKGREEN + message + Colors.ENDC) - -def print_error(message): - print(Colors.FAIL + message + Colors.ENDC) - -def print_connection_error(): - print(Colors.FAIL + "Failed to connect to the service! Please check if it's running and accessible." + Colors.ENDC) - -def get_user_choice(): - sources = [] - print(Colors.HEADER + "Available instances to import to:" + Colors.ENDC) - - # Add master installations - for app in master_config: - sources.append((app, f"{app.capitalize()} [Master]")) - - # Add extra installations - if "extras" in config['instances']: - for app, instances in config['instances']['extras'].items(): - for install in instances: - sources.append((app, f"{app.capitalize()} [{install['name']}]")) - - # Display sources with numbers - for idx, (app, name) in enumerate(sources, start=1): - print(f"{idx}. {name}") - - # User selection - choice = input("Enter the number of the instance to import to: ").strip() - while not choice.isdigit() or int(choice) < 1 or int(choice) > len(sources): - print_error("Invalid input. Please enter a valid number.") - choice = input("Enter the number of the instance to import to: ").strip() - - selected_app, selected_name = sources[int(choice) - 1] - print() - return selected_app, selected_name - -def get_import_choice(): - print() - print(Colors.HEADER + "Choose what to import:" + Colors.ENDC) - print("1. Custom Formats") - print("2. Quality Profiles") - choice = input("Enter your choice (1/2): ").strip() - while choice not in ["1", "2"]: - print_error("Invalid input. Please enter 1 or 2.") - choice = input("Enter your choice (1/2): ").strip() - return choice - -def get_app_config(app_name, instance_name): - if instance_name.endswith("[Master]"): - return master_config[app_name] +def get_custom_formats(app): + config = load_config() + import_path = f"{config['settings']['import_path']}/custom_formats/{app}" # Adjusted path + for file in os.listdir(import_path): + if fnmatch.fnmatch(file, f'*{app}*'): + return file + return None + + +def process_format(format, existing_names_to_id, base_url, api_key): + format_name = format['name'] + if format_name in existing_names_to_id: + format_id = existing_names_to_id[format_name] + response = make_request('put', base_url, api_key, f'customformat/{format_id}', format) + if response is not None: + print_message(f"Updating custom format '{format_name}'", "yellow", newline=False) + print_message(" : SUCCESS", "green") + return 1, 0 + else: + print_message(f"Updating custom format '{format_name}'", "yellow", newline=False) + print_message(" : FAIL", "red", newline=False) 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.") - -def select_file(directory, app_name, sync_mode=False): - app_name = app_name.lower() - files = [f for f in os.listdir(directory) if os.path.isfile(os.path.join(directory, f)) and app_name in f.lower()] - if not files: - print_error(f"No files found for {app_name.capitalize()} in {directory}.") - return None - - if sync_mode: - # Automatically select all files in sync mode - return files + response = make_request('post', base_url, api_key, 'customformat', format) + if response is not None: + print_message(f"Adding custom format '{format_name}'", "blue", newline=False) + print_message(" : SUCCESS", "green") + return 0, 1 + else: + print_message(f"Adding custom format '{format_name}'", "blue", newline=False) + print_message(" : FAIL", "red", newline=False) + return 0, 0 - print() - print(Colors.OKBLUE + "Available files:" + Colors.ENDC) - for i, file in enumerate(files, 1): - print(f"{i}. {file}") +def import_custom_formats(app, instances): + + config = load_config() + + for instance in instances: + api_key = instance['api_key'] + base_url = instance['base_url'] + + existing_formats = make_request('get', base_url, api_key, 'customformat') + existing_names_to_id = {format['name']: format['id'] for format in existing_formats} + + app_file = get_custom_formats(app) + if app_file is None: + print_message(f"No file found for app: {app}", "red") + continue + + added_count, updated_count = 0, 0 + with open(f"{config['settings']['import_path']}/custom_formats/{app}/{app_file}", 'r') as import_file: + import_formats = json.load(import_file) + + print_message(f"Importing custom formats to {app} : {instance['name']}", "purple") + print() + + for format in import_formats: + added, updated = process_format(format, existing_names_to_id, base_url, api_key) + added_count += added + updated_count += updated - choice = input("Select a file to import (or 'all' for all files): ").strip() - print() - if choice.isdigit() and 1 <= int(choice) <= len(files): - return [files[int(choice) - 1]] - elif choice.lower() == 'all': - return files - else: - print_error("Invalid input. Please enter a valid number or 'all'.") print() - return None - - - -def import_custom_formats(source_config, import_path='./custom_formats', selected_files=None, sync_mode=False): - 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: - existing_formats = response.json() - existing_names_to_id = {format['name']: format['id'] for format in existing_formats} - - if selected_files is None: - selected_files = select_file(import_path, source_config['app_name'], sync_mode=sync_mode) - if not selected_files: - return # Exit if no file is selected - - for selected_file in selected_files: - added_count, updated_count = 0, 0 - with open(os.path.join(import_path, selected_file), 'r') as import_file: - import_formats = json.load(import_file) - - for format in import_formats: - format_name = format['name'] - if format_name in existing_names_to_id: - format_id = existing_names_to_id[format_name] - put_url = f"{source_config['base_url']}/api/v3/customformat/{format_id}" - response = requests.put(put_url, json=format, headers=headers) - if response.status_code in [200, 201, 202]: - print(Colors.WARNING + f"Updating custom format '{format_name}': " + Colors.ENDC, end='') - print_success("SUCCESS") - updated_count += 1 - else: - print_error(f"Updating custom format '{format_name}': FAIL") - print(response.content.decode()) - - else: - post_url = f"{source_config['base_url']}/api/v3/customformat" - response = requests.post(post_url, json=format, headers=headers) - if response.status_code in [200, 201]: - print(Colors.OKBLUE + f"Adding custom format '{format_name}': " + Colors.ENDC, end='') - print_success("SUCCESS") - added_count += 1 - else: - print_error(f"Adding custom format '{format_name}': FAIL") - print(response.content.decode()) - - print() - print_success(f"Successfully added {added_count} custom formats, updated {updated_count} custom formats.") + print_message( + f"Successfully added {added_count} custom formats, " + f"updated {updated_count} custom formats.", + "purple" + ) + print() +def get_profiles(app): + config = load_config() + import_path = f"{config['settings']['import_path']}/quality_profiles/{app.lower()}" # Adjusted path + matching_files = [] # Create an empty list to hold matching files + for file in os.listdir(import_path): + if fnmatch.fnmatch(file, f'*{app}*'): + matching_files.append(file) # Add matching file to the list + return matching_files # Return the list of matching files + +def get_existing_profiles(base_url, api_key): + resource_type = 'qualityprofile' + existing_profiles = make_request('get', base_url, api_key, resource_type) + + + return {profile['name']: profile for profile in existing_profiles} if existing_profiles else {} + +def cf_import_sync(instances): + for instance in instances: + api_key = instance['api_key'] + base_url = instance['base_url'] + resource_type = 'customformat' + response = make_request('get', base_url, api_key, resource_type) + + if response: + instance['custom_formats'] = {format['name']: format['id'] for format in response} else: - print_error(f"Failed to retrieve existing custom formats from {get_url}! (HTTP {response.status_code})") - print(response.content.decode()) + print_message("No custom formats found for this instance.", "purple") + print() + +def user_select_profiles(profiles): + print_message("Available profiles:", "purple") + for idx, profile in enumerate(profiles, start=1): + print(f"{idx}. {profile}") + print() + + while True: + # Display prompt message + print_message("Enter the numbers of the profiles you want to import separated by commas, or type 'all' to import all profiles: ", "blue", newline=False) + print() + user_input = input().strip() + + if user_input.lower() == 'all': + return profiles # Return all profiles if 'all' is selected + + selected_profiles = [] + try: + selected_indices = [int(index.strip()) for index in user_input.split(',')] + for index in selected_indices: + if 1 <= index <= len(profiles): + selected_profiles.append(profiles[index - 1]) + else: + raise ValueError(f"Invalid selection: {index}. Please enter valid numbers.") # Raise an error to trigger except block + return selected_profiles # Return the selected profiles if all inputs are valid + except ValueError as e: + print_message(str(e), "red") # Display error message in red + + + +def process_profile(profile, base_url, api_key, custom_formats, existing_profiles): + profile_name = profile.get('name') + existing_profile = existing_profiles.get(profile_name) + + # Update or add custom format items as needed + if 'formatItems' in profile: + for format_item in profile['formatItems']: + format_name = format_item.get('name') + if format_name and format_name in custom_formats: + format_item['format'] = custom_formats[format_name] + + for format_name, format_id in custom_formats.items(): + if format_name not in {item.get('name') for item in profile.get('formatItems', [])}: + profile.setdefault('formatItems', []).append({ + "format": format_id, + "name": format_name, + "score": 0 + }) + + if existing_profile: + profile['id'] = existing_profile['id'] + action = "Updating" + action_color = "yellow" + resource_type = f"qualityprofile/{profile['id']}" + else: + action = "Adding" + action_color = "blue" + resource_type = "qualityprofile" + + response = make_request('put' if existing_profile else 'post', base_url, api_key, resource_type, profile) - except requests.exceptions.ConnectionError: - print_connection_error() + # Print the action statement in blue for Adding and yellow for Updating + print_message(f"{action} '{profile_name}' quality profile", action_color, newline=False) + # Determine the status and print the status in green (OK) or red (FAIL) + if response: + print_message(" : SUCCESS", "green") + else: + print_message(" : FAIL", "red") +def import_quality_profiles(app, instances): -def import_quality_profiles(source_config, import_path='./profiles', selected_files=None, sync_mode=False): - headers = {"X-Api-Key": source_config['api_key']} + config = load_config() - try: - cf_import_sync(source_config) + cf_import_sync(instances) - if not selected_files: - if sync_mode: - # Automatically select all profile files - selected_files = [f for f in os.listdir(import_path) if os.path.isfile(os.path.join(import_path, f))] + all_profiles = get_profiles(app) + selected_profiles_names = user_select_profiles(all_profiles) - if not selected_files: - return # Exit if no file is selected + for instance in instances: + base_url = instance['base_url'] + api_key = instance['api_key'] + custom_formats = instance.get('custom_formats', {}) + existing_profiles = get_existing_profiles(base_url, api_key) # Retrieve existing profiles - for selected_file in selected_files: - with open(os.path.join(import_path, selected_file), 'r') as file: + print_message(f"Importing Quality Profiles to {app} : {instance['name']}", "purple") + print() + + for profile_file in selected_profiles_names: + with open(f"{config['settings']['import_path']}/quality_profiles/{app}/{profile_file}", 'r') as file: try: quality_profiles = json.load(file) except json.JSONDecodeError as e: - print_error(f"Error loading selected profile: {e}") + print_message(f"Error loading selected profile: {e}", "red") continue for profile in quality_profiles: - existing_format_names = set() - if 'formatItems' in profile: - for format_item in profile['formatItems']: - format_name = format_item.get('name') - if format_name: - existing_format_names.add(format_name) - if format_name in source_config['custom_formats']: - format_item['format'] = source_config['custom_formats'][format_name] - - for format_name, format_id in source_config['custom_formats'].items(): - if format_name not in existing_format_names: - profile.setdefault('formatItems', []).append({ - "format": format_id, - "name": format_name, - "score": 0 - }) - - post_url = f"{source_config['base_url']}/api/v3/qualityprofile" - response = requests.post(post_url, json=profile, headers=headers) - - if response.status_code in [200, 201]: - print_success(f"Successfully added Quality Profile {profile['name']}") - elif response.status_code == 409: - print_error(f"Failed to add Quality Profile {profile['name']} due to a naming conflict. Quality profile names must be unique. (HTTP {response.status_code})") - else: - try: - errors = response.json() - message = errors.get("message", "No Message Provided") - print_error(f"Failed to add Quality Profile {profile['name']}! (HTTP {response.status_code})") - print(message) - except json.JSONDecodeError: - print_error("Failed to parse error message:") - print(response.text) - - except requests.exceptions.ConnectionError: - print_connection_error() - - - - -def cf_import_sync(source_config): - headers = {"X-Api-Key": source_config['api_key']} - custom_format_url = f"{source_config['base_url']}/api/v3/customformat" - try: - response = requests.get(custom_format_url, headers=headers) - if response.status_code == 200: - data = response.json() - source_config['custom_formats'] = {format['name']: format['id'] for format in data} - elif response.status_code == 401: - print_error("Authentication error: Invalid API key. Terminating program.") - exit(1) - else: - print_error(f"Failed to retrieve custom formats! (HTTP {response.status_code})") - print(response.content.decode()) - exit(1) + process_profile(profile, base_url, api_key, custom_formats, existing_profiles) + + print() - except requests.exceptions.ConnectionError: - print_connection_error() - exit(1) +def main(): + app = get_app_choice() + instances = get_instance_choice(app) + import_custom_formats(app, instances) + import_quality_profiles(app, instances) 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 - import_choice = get_import_choice() - - if import_choice == "1": - selected_files = select_file('./custom_formats', selected_app) - if selected_files: - import_custom_formats(source_config, './custom_formats', selected_files) - elif import_choice == "2": - selected_files = select_file('./profiles', selected_app) - if selected_files: - import_quality_profiles(source_config, './profiles', selected_files) \ No newline at end of file + main() \ No newline at end of file diff --git a/custom_formats/Custom Formats (Radarr).json b/imports/custom_formats/radarr/Custom Formats (Radarr).json similarity index 98% rename from custom_formats/Custom Formats (Radarr).json rename to imports/custom_formats/radarr/Custom Formats (Radarr).json index a008c2a..5346c95 100644 --- a/custom_formats/Custom Formats (Radarr).json +++ b/imports/custom_formats/radarr/Custom Formats (Radarr).json @@ -8949,6 +8949,130 @@ "isFloat": false } ] + }, + { + "name": "WEB", + "implementation": "SourceSpecification", + "implementationName": "Source", + "infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2", + "negate": true, + "required": false, + "fields": [ + { + "order": 0, + "name": "value", + "label": "Source", + "value": 7, + "type": "select", + "advanced": false, + "selectOptions": [ + { + "value": 0, + "name": "UNKNOWN", + "order": 0, + "dividerAfter": false + }, + { + "value": 1, + "name": "CAM", + "order": 1, + "dividerAfter": false + }, + { + "value": 2, + "name": "TELESYNC", + "order": 2, + "dividerAfter": false + }, + { + "value": 3, + "name": "TELECINE", + "order": 3, + "dividerAfter": false + }, + { + "value": 4, + "name": "WORKPRINT", + "order": 4, + "dividerAfter": false + }, + { + "value": 5, + "name": "DVD", + "order": 5, + "dividerAfter": false + }, + { + "value": 6, + "name": "TV", + "order": 6, + "dividerAfter": false + }, + { + "value": 7, + "name": "WEBDL", + "order": 7, + "dividerAfter": false + }, + { + "value": 8, + "name": "WEBRIP", + "order": 8, + "dividerAfter": false + }, + { + "value": 9, + "name": "BLURAY", + "order": 9, + "dividerAfter": false + } + ], + "privacy": "normal", + "isFloat": false + } + ] + }, + { + "name": "AMiABLE", + "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.-])AMiABLE\\b", + "type": "textbox", + "advanced": false, + "privacy": "normal", + "isFloat": false + } + ] + }, + { + "name": "PiGNUS", + "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.-])PiGNUS\\b", + "type": "textbox", + "advanced": false, + "privacy": "normal", + "isFloat": false + } + ] } ] }, @@ -12794,5 +12918,32 @@ ] } ] + }, + { + "name": "TEST FLAC", + "includeCustomFormatWhenRenaming": false, + "specifications": [ + { + "name": "flac", + "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": "[.\\- ]FLAC", + "type": "textbox", + "advanced": false, + "privacy": "normal", + "isFloat": false + } + ] + } + ] } ] \ No newline at end of file diff --git a/custom_formats/Custom Formats (Sonarr).json b/imports/custom_formats/sonarr/Custom Formats (Sonarr).json similarity index 100% rename from custom_formats/Custom Formats (Sonarr).json rename to imports/custom_formats/sonarr/Custom Formats (Sonarr).json diff --git a/profiles/1080p Balanced (Radarr).json b/imports/quality_profiles/radarr/1080p Balanced (Radarr).json similarity index 98% rename from profiles/1080p Balanced (Radarr).json rename to imports/quality_profiles/radarr/1080p Balanced (Radarr).json index 1317684..21111d4 100644 --- a/profiles/1080p Balanced (Radarr).json +++ b/imports/quality_profiles/radarr/1080p Balanced (Radarr).json @@ -366,6 +366,21 @@ "minFormatScore": 0, "cutoffFormatScore": 500, "formatItems": [ + { + "format": 338, + "name": "TEST FLAC", + "score": 0 + }, + { + "format": 337, + "name": "h265 (4k)", + "score": -9999 + }, + { + "format": 336, + "name": "MAX", + "score": 0 + }, { "format": 335, "name": "Blu-Ray (Remux)", diff --git a/profiles/1080p Balanced (Single Grab) (Radarr).json b/imports/quality_profiles/radarr/1080p Balanced (Single Grab) (Radarr).json similarity index 98% rename from profiles/1080p Balanced (Single Grab) (Radarr).json rename to imports/quality_profiles/radarr/1080p Balanced (Single Grab) (Radarr).json index d9dc0ae..eece6e2 100644 --- a/profiles/1080p Balanced (Single Grab) (Radarr).json +++ b/imports/quality_profiles/radarr/1080p Balanced (Single Grab) (Radarr).json @@ -366,6 +366,21 @@ "minFormatScore": 0, "cutoffFormatScore": 0, "formatItems": [ + { + "format": 338, + "name": "TEST FLAC", + "score": 0 + }, + { + "format": 337, + "name": "h265 (4k)", + "score": 0 + }, + { + "format": 336, + "name": "MAX", + "score": 0 + }, { "format": 335, "name": "Blu-Ray (Remux)", diff --git a/profiles/1080p Optimal (Radarr).json b/imports/quality_profiles/radarr/1080p Optimal (Radarr).json similarity index 98% rename from profiles/1080p Optimal (Radarr).json rename to imports/quality_profiles/radarr/1080p Optimal (Radarr).json index 796acc4..6dcd8d3 100644 --- a/profiles/1080p Optimal (Radarr).json +++ b/imports/quality_profiles/radarr/1080p Optimal (Radarr).json @@ -359,6 +359,21 @@ "minFormatScore": 0, "cutoffFormatScore": 1000, "formatItems": [ + { + "format": 338, + "name": "TEST FLAC", + "score": 0 + }, + { + "format": 337, + "name": "h265 (4k)", + "score": 0 + }, + { + "format": 336, + "name": "MAX", + "score": 0 + }, { "format": 335, "name": "Blu-Ray (Remux)", diff --git a/profiles/1080p Optimal (Single Grab) (Radarr).json b/imports/quality_profiles/radarr/1080p Optimal (Single Grab) (Radarr).json similarity index 98% rename from profiles/1080p Optimal (Single Grab) (Radarr).json rename to imports/quality_profiles/radarr/1080p Optimal (Single Grab) (Radarr).json index 4fd5da8..ec9ba8b 100644 --- a/profiles/1080p Optimal (Single Grab) (Radarr).json +++ b/imports/quality_profiles/radarr/1080p Optimal (Single Grab) (Radarr).json @@ -359,6 +359,21 @@ "minFormatScore": 0, "cutoffFormatScore": 0, "formatItems": [ + { + "format": 338, + "name": "TEST FLAC", + "score": 0 + }, + { + "format": 337, + "name": "h265 (4k)", + "score": 0 + }, + { + "format": 336, + "name": "MAX", + "score": 0 + }, { "format": 335, "name": "Blu-Ray (Remux)", diff --git a/profiles/1080p Transparent (Double Grab) (Radarr).json b/imports/quality_profiles/radarr/1080p Transparent (Double Grab) (Radarr).json similarity index 98% rename from profiles/1080p Transparent (Double Grab) (Radarr).json rename to imports/quality_profiles/radarr/1080p Transparent (Double Grab) (Radarr).json index b57b855..d63b085 100644 --- a/profiles/1080p Transparent (Double Grab) (Radarr).json +++ b/imports/quality_profiles/radarr/1080p Transparent (Double Grab) (Radarr).json @@ -366,6 +366,21 @@ "minFormatScore": 0, "cutoffFormatScore": 140, "formatItems": [ + { + "format": 338, + "name": "TEST FLAC", + "score": 0 + }, + { + "format": 337, + "name": "h265 (4k)", + "score": 0 + }, + { + "format": 336, + "name": "MAX", + "score": 0 + }, { "format": 335, "name": "Blu-Ray (Remux)", diff --git a/profiles/1080p Transparent (Radarr).json b/imports/quality_profiles/radarr/1080p Transparent (Radarr).json similarity index 98% rename from profiles/1080p Transparent (Radarr).json rename to imports/quality_profiles/radarr/1080p Transparent (Radarr).json index 40dcbf8..f9444b2 100644 --- a/profiles/1080p Transparent (Radarr).json +++ b/imports/quality_profiles/radarr/1080p Transparent (Radarr).json @@ -366,6 +366,21 @@ "minFormatScore": 0, "cutoffFormatScore": 500, "formatItems": [ + { + "format": 338, + "name": "TEST FLAC", + "score": 0 + }, + { + "format": 337, + "name": "h265 (4k)", + "score": 0 + }, + { + "format": 336, + "name": "MAX", + "score": 0 + }, { "format": 335, "name": "Blu-Ray (Remux)", diff --git a/profiles/1080p Transparent (Single Grab) (Radarr).json b/imports/quality_profiles/radarr/1080p Transparent (Single Grab) (Radarr).json similarity index 98% rename from profiles/1080p Transparent (Single Grab) (Radarr).json rename to imports/quality_profiles/radarr/1080p Transparent (Single Grab) (Radarr).json index 2246e72..f3b334d 100644 --- a/profiles/1080p Transparent (Single Grab) (Radarr).json +++ b/imports/quality_profiles/radarr/1080p Transparent (Single Grab) (Radarr).json @@ -366,6 +366,21 @@ "minFormatScore": 0, "cutoffFormatScore": 0, "formatItems": [ + { + "format": 338, + "name": "TEST FLAC", + "score": 0 + }, + { + "format": 337, + "name": "h265 (4k)", + "score": 0 + }, + { + "format": 336, + "name": "MAX", + "score": 0 + }, { "format": 335, "name": "Blu-Ray (Remux)", diff --git a/profiles/1080p h265 Balanced (Radarr).json b/imports/quality_profiles/radarr/1080p h265 Balanced (Radarr).json similarity index 98% rename from profiles/1080p h265 Balanced (Radarr).json rename to imports/quality_profiles/radarr/1080p h265 Balanced (Radarr).json index 114e8c5..d2b4750 100644 --- a/profiles/1080p h265 Balanced (Radarr).json +++ b/imports/quality_profiles/radarr/1080p h265 Balanced (Radarr).json @@ -366,6 +366,21 @@ "minFormatScore": 0, "cutoffFormatScore": 1000, "formatItems": [ + { + "format": 338, + "name": "TEST FLAC", + "score": 0 + }, + { + "format": 337, + "name": "h265 (4k)", + "score": 0 + }, + { + "format": 336, + "name": "MAX", + "score": 0 + }, { "format": 335, "name": "Blu-Ray (Remux)", diff --git a/profiles/1080p h265 Balanced (Single Grab) (Radarr).json b/imports/quality_profiles/radarr/1080p h265 Balanced (Single Grab) (Radarr).json similarity index 98% rename from profiles/1080p h265 Balanced (Single Grab) (Radarr).json rename to imports/quality_profiles/radarr/1080p h265 Balanced (Single Grab) (Radarr).json index 7e339c7..be9a5e1 100644 --- a/profiles/1080p h265 Balanced (Single Grab) (Radarr).json +++ b/imports/quality_profiles/radarr/1080p h265 Balanced (Single Grab) (Radarr).json @@ -366,6 +366,21 @@ "minFormatScore": 0, "cutoffFormatScore": 0, "formatItems": [ + { + "format": 338, + "name": "TEST FLAC", + "score": 0 + }, + { + "format": 337, + "name": "h265 (4k)", + "score": 0 + }, + { + "format": 336, + "name": "MAX", + "score": 0 + }, { "format": 335, "name": "Blu-Ray (Remux)", diff --git a/profiles/2160p Optimal (Radarr).json b/imports/quality_profiles/radarr/2160p Optimal (Radarr).json similarity index 96% rename from profiles/2160p Optimal (Radarr).json rename to imports/quality_profiles/radarr/2160p Optimal (Radarr).json index e23b1df..c4f4685 100644 --- a/profiles/2160p Optimal (Radarr).json +++ b/imports/quality_profiles/radarr/2160p Optimal (Radarr).json @@ -359,10 +359,50 @@ "minFormatScore": 0, "cutoffFormatScore": 320, "formatItems": [ + { + "format": 344, + "name": "jennaortegaUHD", + "score": -99999 + }, + { + "format": 342, + "name": "Freeleech25", + "score": 3 + }, + { + "format": 341, + "name": "Freeleech50", + "score": 2 + }, + { + "format": 340, + "name": "Freeleech75", + "score": 1 + }, + { + "format": 339, + "name": "Freeleech100", + "score": 4 + }, + { + "format": 338, + "name": "TEST FLAC", + "score": 0 + }, + { + "format": 337, + "name": "h265 (4k)", + "score": 0 + }, + { + "format": 336, + "name": "MAX", + "score": 0 + }, { "format": 335, "name": "Blu-Ray (Remux)", - "score": 0 + "score": 60 }, { "format": 334, diff --git a/profiles/2160p Optimal (Single Grab) (Radarr).json b/imports/quality_profiles/radarr/2160p Optimal (Single Grab) (Radarr).json similarity index 96% rename from profiles/2160p Optimal (Single Grab) (Radarr).json rename to imports/quality_profiles/radarr/2160p Optimal (Single Grab) (Radarr).json index c918936..6a48d0d 100644 --- a/profiles/2160p Optimal (Single Grab) (Radarr).json +++ b/imports/quality_profiles/radarr/2160p Optimal (Single Grab) (Radarr).json @@ -359,10 +359,50 @@ "minFormatScore": 0, "cutoffFormatScore": 0, "formatItems": [ + { + "format": 344, + "name": "jennaortegaUHD", + "score": -9999 + }, + { + "format": 342, + "name": "Freeleech25", + "score": 0 + }, + { + "format": 341, + "name": "Freeleech50", + "score": 0 + }, + { + "format": 340, + "name": "Freeleech75", + "score": 0 + }, + { + "format": 339, + "name": "Freeleech100", + "score": 0 + }, + { + "format": 338, + "name": "TEST FLAC", + "score": 0 + }, + { + "format": 337, + "name": "h265 (4k)", + "score": 0 + }, + { + "format": 336, + "name": "MAX", + "score": 0 + }, { "format": 335, "name": "Blu-Ray (Remux)", - "score": 0 + "score": 60 }, { "format": 334, diff --git a/profiles/1080p Balanced (Single Grab) (Sonarr).json b/imports/quality_profiles/sonarr/1080p Balanced (Single Grab) (Sonarr).json similarity index 100% rename from profiles/1080p Balanced (Single Grab) (Sonarr).json rename to imports/quality_profiles/sonarr/1080p Balanced (Single Grab) (Sonarr).json diff --git a/profiles/1080p Balanced (Sonarr).json b/imports/quality_profiles/sonarr/1080p Balanced (Sonarr).json similarity index 100% rename from profiles/1080p Balanced (Sonarr).json rename to imports/quality_profiles/sonarr/1080p Balanced (Sonarr).json diff --git a/profiles/1080p Optimal (Single Grab) (Sonarr).json b/imports/quality_profiles/sonarr/1080p Optimal (Single Grab) (Sonarr).json similarity index 100% rename from profiles/1080p Optimal (Single Grab) (Sonarr).json rename to imports/quality_profiles/sonarr/1080p Optimal (Single Grab) (Sonarr).json diff --git a/profiles/1080p Optimal (Sonarr).json b/imports/quality_profiles/sonarr/1080p Optimal (Sonarr).json similarity index 100% rename from profiles/1080p Optimal (Sonarr).json rename to imports/quality_profiles/sonarr/1080p Optimal (Sonarr).json diff --git a/profiles/1080p Transparent (Double Grab) (Sonarr).json b/imports/quality_profiles/sonarr/1080p Transparent (Double Grab) (Sonarr).json similarity index 100% rename from profiles/1080p Transparent (Double Grab) (Sonarr).json rename to imports/quality_profiles/sonarr/1080p Transparent (Double Grab) (Sonarr).json diff --git a/profiles/1080p Transparent (Single Grab) (Sonarr).json b/imports/quality_profiles/sonarr/1080p Transparent (Single Grab) (Sonarr).json similarity index 100% rename from profiles/1080p Transparent (Single Grab) (Sonarr).json rename to imports/quality_profiles/sonarr/1080p Transparent (Single Grab) (Sonarr).json diff --git a/profiles/1080p Transparent (Sonarr).json b/imports/quality_profiles/sonarr/1080p Transparent (Sonarr).json similarity index 100% rename from profiles/1080p Transparent (Sonarr).json rename to imports/quality_profiles/sonarr/1080p Transparent (Sonarr).json diff --git a/profiles/1080p h265 Balanced (Sonarr).json b/imports/quality_profiles/sonarr/1080p h265 Balanced (Sonarr).json similarity index 100% rename from profiles/1080p h265 Balanced (Sonarr).json rename to imports/quality_profiles/sonarr/1080p h265 Balanced (Sonarr).json diff --git a/profiles/1080p h265 Balanced (Single Grab) (Sonarr).json b/imports/quality_profiles/sonarr/1080p h265 Balanced (Single Grab) (Sonarr).json similarity index 100% rename from profiles/1080p h265 Balanced (Single Grab) (Sonarr).json rename to imports/quality_profiles/sonarr/1080p h265 Balanced (Single Grab) (Sonarr).json diff --git a/profiles/2160p Optimal (Single Grab) (Sonarr).json b/imports/quality_profiles/sonarr/2160p Optimal (Single Grab) (Sonarr).json similarity index 98% rename from profiles/2160p Optimal (Single Grab) (Sonarr).json rename to imports/quality_profiles/sonarr/2160p Optimal (Single Grab) (Sonarr).json index 2c4630a..07934ee 100644 --- a/profiles/2160p Optimal (Single Grab) (Sonarr).json +++ b/imports/quality_profiles/sonarr/2160p Optimal (Single Grab) (Sonarr).json @@ -239,6 +239,21 @@ "minFormatScore": 0, "cutoffFormatScore": 0, "formatItems": [ + { + "format": 229, + "name": "HR", + "score": 0 + }, + { + "format": 228, + "name": "MAX", + "score": 0 + }, + { + "format": 227, + "name": "h265 (4k)", + "score": 0 + }, { "format": 226, "name": "PCM", @@ -247,7 +262,7 @@ { "format": 225, "name": "Blu-Ray (Remux)", - "score": 0 + "score": 60 }, { "format": 224, diff --git a/profiles/2160p Optimal (Sonarr).json b/imports/quality_profiles/sonarr/2160p Optimal (Sonarr).json similarity index 98% rename from profiles/2160p Optimal (Sonarr).json rename to imports/quality_profiles/sonarr/2160p Optimal (Sonarr).json index 37ede33..98a54b6 100644 --- a/profiles/2160p Optimal (Sonarr).json +++ b/imports/quality_profiles/sonarr/2160p Optimal (Sonarr).json @@ -239,6 +239,21 @@ "minFormatScore": 0, "cutoffFormatScore": 320, "formatItems": [ + { + "format": 229, + "name": "HR", + "score": 0 + }, + { + "format": 228, + "name": "MAX", + "score": 0 + }, + { + "format": 227, + "name": "h265 (4k)", + "score": 0 + }, { "format": 226, "name": "PCM", @@ -247,7 +262,7 @@ { "format": 225, "name": "Blu-Ray (Remux)", - "score": 0 + "score": 60 }, { "format": 224, diff --git a/requirements.txt b/requirements.txt index 42a803dcaa2bee24bca5623dad4625d0d0431e97..fdff9a991e1747ec8bb18ba632b7a2ff916671ff 100644 GIT binary patch literal 31 mcmWHjjCAz%v9&eRGte{S3Q8?3O)V}dwzW0VGd9#S-~s@TTL^Xl literal 254 zcmX|+y9&ZU5JgWd_$kU}BM}P=dj(s|7&R_LgLxGC@#>usWSD`ykJ&q)cg2~Gfy70< zRz-q3XHKTFxn#a1T~UZJRpuKy{r6zh?3Jc>MHHKR=HQx9`5I4m6#l!-{I