mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-26 12:52:00 +01:00
init
This commit is contained in:
41
README.md
41
README.md
@@ -1,2 +1,41 @@
|
||||
# Profilarr
|
||||
Profilarr is a synchronization tool designed for users of the "arr" suite, including Radarr and Sonarr. It offers seamless import and synchronization of quality profiles and custom formats via the API, allowing users to maintain uniform settings across multiple instances with minimal hassle.
|
||||
|
||||
Profilarr is a Python-based tool that enables seamless synchronization of custom formats and quality profiles in Radarr / Sonarr. It's designed to aid users in maintaining consistent configurations across different environments or instances of Radarr.
|
||||
|
||||
## ⚠️ Before Continuing
|
||||
|
||||
- **This tool will overwrite any custom formats in your Radarr installation that have the same name.**
|
||||
- **Custom Formats MUST be imported before syncing a profile.**
|
||||
|
||||
## 🛠️ Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.x installed. You can download it from [python.org](https://www.python.org/downloads/).
|
||||
|
||||
### Steps
|
||||
|
||||
1. Download the Profilarr zip file from the release section.
|
||||
2. Extract its contents into a folder.
|
||||
3. Open `import.py` in a text editor of your choice.
|
||||
- Add your Radarr API key to the designated section.
|
||||
- Modify the Base URL if needed
|
||||
4. Save the changes and close the text editor.
|
||||
|
||||
## 🚀 Usage
|
||||
|
||||
1. Open a terminal or command prompt.
|
||||
2. Navigate to the directory where you extracted Profilarr.
|
||||
3. Run the command `python import_cf.py` to import the necessary custom formats.
|
||||
4. Run the command `python import_qf.py` and follow the prompts to choose and import your desired profile.
|
||||
|
||||
## 📦 Dependencies
|
||||
|
||||
- `requests` (Install using `pip install requests`)
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
### Radarr API Key and Base URL
|
||||
|
||||
- Your Radarr API Key and Base URL can be configured in the `import.py` file.
|
||||
- The Base URL should be in the format `http://localhost:7878` unless you have a different host or port.
|
||||
|
||||
11879
custom_formats/cf.json
Normal file
11879
custom_formats/cf.json
Normal file
File diff suppressed because it is too large
Load Diff
85
export.py
Normal file
85
export.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import json
|
||||
import requests
|
||||
import os
|
||||
import re
|
||||
|
||||
# Define constants
|
||||
base_url = "http://localhost:7878"
|
||||
api_key = "API_GOES_HERE"
|
||||
|
||||
# Define parameters and headers
|
||||
params = {"apikey": api_key}
|
||||
headers = {"X-Api-Key": api_key}
|
||||
files = {'file': ('', '')} # Empty file to force multipart/form-data
|
||||
|
||||
# Login
|
||||
login_url = f"{base_url}/login"
|
||||
response = requests.get(login_url, params=params, headers=headers, files=files)
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f"Login Failed! (HTTP {response.status_code})")
|
||||
print("Response Content: ", response.content)
|
||||
exit()
|
||||
|
||||
def export_cf():
|
||||
custom_format_url = f"{base_url}/api/v3/customformat"
|
||||
response = requests.get(custom_format_url, params=params, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
|
||||
# Remove 'id' from each custom format
|
||||
for custom_format in data:
|
||||
custom_format.pop('id', None)
|
||||
|
||||
# Ensure the ./custom_formats directory exists
|
||||
if not os.path.exists('./custom_formats'):
|
||||
os.makedirs('./custom_formats')
|
||||
|
||||
# Save to JSON file
|
||||
with open('./custom_formats/cf.json', 'w') as f:
|
||||
json.dump(data, f, indent=4)
|
||||
print("Custom Formats have been saved to './custom_formats/cf.json'")
|
||||
else:
|
||||
print(f"Failed to retrieve custom formats! (HTTP {response.status_code})")
|
||||
print("Response Content: ", response.content)
|
||||
|
||||
def sanitize_filename(filename):
|
||||
# Replace any characters not allowed in filenames with _
|
||||
sanitized_filename = re.sub(r'[\\/*?:"<>|]', '_', filename)
|
||||
return sanitized_filename
|
||||
|
||||
def export_qf():
|
||||
response = requests.get(f"{base_url}/api/v3/qualityprofile", params=params, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
quality_profiles = response.json()
|
||||
|
||||
# Ensure the ./profiles directory exists
|
||||
if not os.path.exists('./profiles'):
|
||||
os.makedirs('./profiles')
|
||||
|
||||
# Process each profile separately
|
||||
for profile in quality_profiles:
|
||||
profile.pop('id', None) # Remove the 'id' field
|
||||
|
||||
# Use the name of the profile to create a filename
|
||||
profile_name = profile.get('name', 'unnamed_profile') # Use a default name if the profile has no name
|
||||
profile_name = sanitize_filename(profile_name) # Sanitize the filename
|
||||
profile_filename = f"{profile_name}.json"
|
||||
profile_filepath = os.path.join('./profiles', profile_filename)
|
||||
|
||||
# Save the individual profile to a file as a single-element array
|
||||
with open(profile_filepath, 'w') as file:
|
||||
json.dump([profile], file, indent=4) # Note the [profile], it will make it an array with a single element
|
||||
|
||||
print("Quality profiles have been successfully saved to the ./profiles directory")
|
||||
else:
|
||||
print("Failed to retrieve quality profiles!")
|
||||
print("Response Content: ", response.content.decode('utf-8'))
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
export_cf()
|
||||
export_qf()
|
||||
60
import_cf.py
Normal file
60
import_cf.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import json
|
||||
import requests
|
||||
|
||||
# Define constants
|
||||
base_url = "http://localhost:7878" # Update to your Radarr URL
|
||||
api_key = "API_GOES_HERE" # Update to your Radarr API Key
|
||||
|
||||
# Define headers
|
||||
headers = {"X-Api-Key": api_key}
|
||||
|
||||
def get_existing_formats():
|
||||
get_url = f"{base_url}/api/v3/customformat"
|
||||
print(f"Getting existing formats from {get_url}")
|
||||
response = requests.get(get_url, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
with open('temp_cf.json', 'w') as temp_file:
|
||||
json.dump(response.json(), temp_file)
|
||||
else:
|
||||
print(f"Failed to retrieve existing custom formats from {get_url}! (HTTP {response.status_code})")
|
||||
print("Response Content: \n", response.content.decode())
|
||||
exit(1)
|
||||
|
||||
def import_custom_formats():
|
||||
with open('temp_cf.json', 'r') as temp_file:
|
||||
existing_formats = json.load(temp_file)
|
||||
existing_names_to_id = {format['name']: format['id'] for format in existing_formats}
|
||||
|
||||
with open('custom_formats/cf.json', '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"{base_url}/api/v3/customformat/{format_id}"
|
||||
print(f"Updating existing format {format_name} using PUT at {put_url}")
|
||||
format['id'] = format_id # Include the id in the request body
|
||||
response = requests.put(put_url, json=format, headers=headers)
|
||||
|
||||
if response.status_code in [200, 201, 202]:
|
||||
print(f"Successfully updated custom format {format_name}! (HTTP {response.status_code})")
|
||||
else:
|
||||
print(f"Failed to update custom format {format_name} at {put_url}! (HTTP {response.status_code})")
|
||||
print("Response Content: \n", response.content.decode())
|
||||
|
||||
else:
|
||||
post_url = f"{base_url}/api/v3/customformat"
|
||||
print(f"Creating new format {format_name} using POST at {post_url}")
|
||||
response = requests.post(post_url, json=format, headers=headers)
|
||||
|
||||
if response.status_code in [200, 201]:
|
||||
print(f"Successfully created custom format {format_name}! (HTTP {response.status_code})")
|
||||
else:
|
||||
print(f"Failed to create custom format {format_name} at {post_url}! (HTTP {response.status_code})")
|
||||
print("Response Content: \n", response.content.decode())
|
||||
|
||||
if __name__ == "__main__":
|
||||
get_existing_formats()
|
||||
import_custom_formats()
|
||||
115
import_qf.py
Normal file
115
import_qf.py
Normal file
@@ -0,0 +1,115 @@
|
||||
import json
|
||||
import requests
|
||||
import os # For deleting the temporary file
|
||||
|
||||
# Define constants
|
||||
base_url = "http://localhost:7878" # Update to your Radarr URL
|
||||
api_key = "API_GOES_HERE" # Update to your Radarr API Key
|
||||
|
||||
# Define headers
|
||||
params = {"apikey": api_key}
|
||||
headers = {"X-Api-Key": api_key}
|
||||
|
||||
def cf_import_sync():
|
||||
custom_format_url = f"{base_url}/api/v3/customformat"
|
||||
response = requests.get(custom_format_url, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
with open('custom_formats.json', 'w') as file:
|
||||
json.dump(data, file, indent=4)
|
||||
print("Custom Formats have been saved to 'custom_formats.json'")
|
||||
return True
|
||||
else:
|
||||
print(f"Failed to retrieve custom formats! (HTTP {response.status_code})")
|
||||
print("Response Content: ", response.content.decode('utf-8'))
|
||||
return False
|
||||
|
||||
|
||||
def import_qf():
|
||||
# Call cf_import_sync first
|
||||
cf_import_sync()
|
||||
|
||||
profile_dir = './profiles'
|
||||
profiles = [f for f in os.listdir(profile_dir) if f.endswith('.json')]
|
||||
|
||||
# Prompt user to select a profile
|
||||
print("Available Profiles:")
|
||||
for i, profile in enumerate(profiles, 1):
|
||||
print(f"{i}. {profile}")
|
||||
|
||||
selection = input("Please enter the number of the profile you want to import: ")
|
||||
|
||||
try:
|
||||
selected_file = profiles[int(selection) - 1]
|
||||
except (ValueError, IndexError):
|
||||
print("Invalid selection, please enter a valid number.")
|
||||
return
|
||||
|
||||
# Load the selected profile
|
||||
with open(os.path.join(profile_dir, selected_file), 'r') as file:
|
||||
try:
|
||||
quality_profiles = json.load(file)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error loading selected profile: {e}")
|
||||
return
|
||||
|
||||
# Load custom formats
|
||||
try:
|
||||
with open('custom_formats.json', 'r') as file:
|
||||
custom_formats_data = json.load(file)
|
||||
custom_formats = {format['name']: format['id'] for format in custom_formats_data}
|
||||
except Exception as e:
|
||||
print(f"Failed to load custom formats! Error: {e}")
|
||||
return
|
||||
|
||||
# Process each profile and send requests
|
||||
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 custom_formats:
|
||||
format_item['format'] = custom_formats[format_name]
|
||||
|
||||
for format_name, format_id in 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"{base_url}/api/v3/qualityprofile"
|
||||
response = requests.post(post_url, json=profile, params=params, headers=headers)
|
||||
|
||||
if response.status_code in [200, 201]:
|
||||
print(f"Successfully added Quality Profile {profile['name']}! (HTTP {response.status_code})")
|
||||
else:
|
||||
try:
|
||||
# Assuming the response is JSON, parse it
|
||||
errors = response.json()
|
||||
|
||||
# Extract relevant information from the error message
|
||||
message = errors.get("message", "No Message Provided")
|
||||
description = errors.get("description", "No Description Provided")
|
||||
|
||||
# Format and print the error message
|
||||
print(f"Failed to add Quality Profile {profile['name']}! (HTTP {response.status_code})")
|
||||
print(f"Error Message: {message}")
|
||||
|
||||
except json.JSONDecodeError:
|
||||
# If response is not JSON, print the whole response
|
||||
print("Failed to parse error message:")
|
||||
print(response.text)
|
||||
try:
|
||||
os.remove('custom_formats.json')
|
||||
except FileNotFoundError:
|
||||
pass # File already deleted or does not exist
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import_qf()
|
||||
1008
profiles/Optimal.json
Normal file
1008
profiles/Optimal.json
Normal file
File diff suppressed because it is too large
Load Diff
1015
profiles/Transparent.json
Normal file
1015
profiles/Transparent.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user