Skip to content

Commit

Permalink
v0.3 (#29)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
santiagosayshey authored Feb 5, 2024
1 parent 2e5cabe commit ff79de7
Show file tree
Hide file tree
Showing 33 changed files with 1,151 additions and 862 deletions.
391 changes: 166 additions & 225 deletions README.md

Large diffs are not rendered by default.

249 changes: 93 additions & 156 deletions deletarr.py
Original file line number Diff line number Diff line change
@@ -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()
34 changes: 24 additions & 10 deletions develop/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit ff79de7

Please sign in to comment.