mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
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.
This commit is contained in:
373
README.md
373
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
|
||||
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.
|
||||
|
||||
```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
|
||||
#### 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.
|
||||
```
|
||||
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
|
||||
|
||||
#### Quality Profile Import Example
|
||||
Importing custom formats to Radarr : 4k-radarr
|
||||
|
||||
```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
|
||||
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.
|
||||
|
||||
Choose what to import:
|
||||
1. Custom Formats
|
||||
2. Quality Profiles
|
||||
Enter your choice (1/2): 2
|
||||
|
||||
Available files:
|
||||
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
|
||||
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
|
||||
|
||||
Choose what to export:
|
||||
1. Custom Formats
|
||||
2. Quality Profiles
|
||||
3. Both
|
||||
Enter your choice (1/2/3): 3
|
||||
Exporting Custom Formats for Radarr : 4k-radarr
|
||||
Exported 134 custom formats to ./exports/custom_formats/Radarr for 4k-radarr
|
||||
|
||||
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>
|
||||
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'
|
||||
Enter your choice:
|
||||
1
|
||||
Exporting Custom Formats for radarr : Master
|
||||
Exported 134 custom formats to ./exports\custom_formats\radarr for Master
|
||||
|
||||
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'
|
||||
Exporting Quality Profiles for radarr : Master...
|
||||
Exported 14 quality profiles to ./exports\quality_profiles\radarr for Master
|
||||
|
||||
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.
|
||||
Importing custom formats to radarr : 4k-radarr
|
||||
|
||||
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>
|
||||
...
|
||||
Updating custom format 'Blu-Ray (Remux)' : SUCCESS
|
||||
Updating custom format 'MAX' : SUCCESS
|
||||
Updating custom format 'h265 (4k)' : SUCCESS
|
||||
Updating custom format 'TEST FLAC' : SUCCESS
|
||||
|
||||
Successfully added 134 custom formats, updated 0 custom formats.
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
### Deleting
|
||||
|
||||
1. Run `python deletarr.py` in your command line interface.
|
||||
2. Select the instance from which you wish to delete data.
|
||||
3. Choose between deleting Custom Formats or Quality Profiles.
|
||||
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 Custom Formats
|
||||
#### Example
|
||||
|
||||
```plaintext
|
||||
PS Z:\Profilarr> python deletarr.py
|
||||
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
|
||||
|
||||
Available instances to delete from:
|
||||
1. Sonarr [Master]
|
||||
2. Radarr [Master]
|
||||
Enter the number of the instance to delete from: 2
|
||||
|
||||
Choose what to delete:
|
||||
Please select what you want to delete:
|
||||
1. Custom Formats
|
||||
2. Quality Profiles
|
||||
Enter your choice (1/2): 1
|
||||
|
||||
Deleting selected custom formats...
|
||||
|
||||
Available items:
|
||||
1. UHDBits
|
||||
2. Dolby Vision w/out Fallback
|
||||
3. Both
|
||||
Enter your choice: 3
|
||||
Available items to delete:
|
||||
1. D-Z0N3
|
||||
2. DON
|
||||
3. EbP
|
||||
4. Geek
|
||||
5. TayTo
|
||||
6. ZQ
|
||||
...
|
||||
132. h265 (4k)
|
||||
133. MAX
|
||||
|
||||
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 custom format 'UHDBits': SUCCESS
|
||||
...
|
||||
Deleting custom format 'MAX': SUCCESS
|
||||
```
|
||||
|
||||
#### Example: Deleting Quality Profiles
|
||||
|
||||
```plaintext
|
||||
PS Z:\Profilarr> python deletarr.py
|
||||
|
||||
Choose what to delete:
|
||||
1. Custom Formats
|
||||
2. Quality Profiles
|
||||
Enter your choice (1/2): 2
|
||||
|
||||
Deleting selected quality profiles...
|
||||
|
||||
Available items:
|
||||
1. 1080p Balanced
|
||||
...
|
||||
11. 2160p Optimal
|
||||
Your choice: all
|
||||
|
||||
Deleting quality profile '1080p Balanced': SUCCESS
|
||||
...
|
||||
Deleting quality profile '2160p Optimal': SUCCESS
|
||||
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
|
||||
|
||||
243
deletarr.py
243
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()
|
||||
print_message("Invalid selection, ignoring.", "red")
|
||||
except ValueError:
|
||||
print_message("Invalid input, please enter numbers only.", "red")
|
||||
|
||||
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"
|
||||
return selected_items
|
||||
|
||||
try:
|
||||
response = requests.get(get_url, headers=headers)
|
||||
if response.status_code == 200:
|
||||
profiles_to_delete = response.json()
|
||||
selected_ids = user_select_items_to_delete(profiles_to_delete)
|
||||
|
||||
for profile_id in selected_ids:
|
||||
delete_url = f"{get_url}/{profile_id}"
|
||||
del_response = requests.delete(delete_url, headers=headers)
|
||||
profile_name = next((item['name'] for item in profiles_to_delete if item['id'] == profile_id), "Unknown")
|
||||
if del_response.status_code in [200, 202, 204]:
|
||||
print(Colors.OKBLUE + f"Deleting quality profile '{profile_name}': " + Colors.ENDC + Colors.OKGREEN + "SUCCESS" + Colors.ENDC)
|
||||
else:
|
||||
# Handle failure due to the profile being in use or other errors
|
||||
error_message = "Failed to delete due to an unknown error."
|
||||
try:
|
||||
# Attempt to parse JSON error message from response
|
||||
error_details = del_response.json()
|
||||
if 'message' in error_details:
|
||||
error_message = error_details['message']
|
||||
elif 'error' in error_details:
|
||||
error_message = error_details['error']
|
||||
except json.JSONDecodeError:
|
||||
# If response is not JSON or doesn't have expected fields
|
||||
error_message = del_response.text or "Failed to delete with no detailed error message."
|
||||
|
||||
print(Colors.OKBLUE + f"Deleting quality profile '{profile_name}': " + Colors.ENDC + Colors.FAIL + f"FAIL - {error_message}" + Colors.ENDC)
|
||||
else:
|
||||
print_error("Failed to retrieve quality profiles for deletion!")
|
||||
except requests.exceptions.ConnectionError:
|
||||
print_connection_error()
|
||||
def 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()
|
||||
|
||||
def get_app_config(app_name, instance_name):
|
||||
if instance_name.endswith("[Master]"):
|
||||
return master_config[app_name]
|
||||
# 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:
|
||||
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.")
|
||||
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_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)
|
||||
|
||||
|
||||
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()
|
||||
|
||||
@@ -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
|
||||
|
||||
258
exportarr.py
258
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'
|
||||
def prompt_export_choice():
|
||||
options = { "1": "Custom Formats", "2": "Quality Profiles" }
|
||||
|
||||
# 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']
|
||||
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")
|
||||
|
||||
def get_user_choice():
|
||||
sources = []
|
||||
print(Colors.HEADER + "Available sources to export from:" + Colors.ENDC)
|
||||
user_choice = input("Your choice: ")
|
||||
|
||||
# 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)
|
||||
if user_choice.lower() == 'all':
|
||||
return list(options.values())
|
||||
else:
|
||||
print(Colors.FAIL + f"An error occurred! (HTTP {response.status_code})" + Colors.ENDC)
|
||||
print("Response Content: ", response.content.decode('utf-8'))
|
||||
return [options[choice] for choice in user_choice.split(',') if choice in options]
|
||||
|
||||
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.")
|
||||
else:
|
||||
for item in items:
|
||||
print(f" - {item}")
|
||||
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 ensure_directory_exists(directory):
|
||||
if not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
print(Colors.OKBLUE + f"Created directory: {directory}" + Colors.ENDC)
|
||||
# Create the directory if it doesn't exist
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
|
||||
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)
|
||||
return dir_path
|
||||
|
||||
base_url, api_key = get_app_config(source)
|
||||
headers = {"X-Api-Key": api_key}
|
||||
params = {"apikey": api_key}
|
||||
def export_custom_formats(app, instances, config):
|
||||
|
||||
print(Colors.OKBLUE + f"Attempting to access {source.capitalize()} at {base_url}" + Colors.ENDC)
|
||||
|
||||
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)
|
||||
url = instance['base_url']
|
||||
api_key = instance['api_key']
|
||||
|
||||
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)
|
||||
# Get the export path from the config
|
||||
export_path = config['settings']['export_path']
|
||||
|
||||
print_saved_items(saved_formats, "Custom Formats")
|
||||
print(Colors.OKGREEN + f"Saved to '{file_path}'" + Colors.ENDC)
|
||||
print()
|
||||
else:
|
||||
handle_response_errors(response)
|
||||
# Create the export directory
|
||||
dir_path = create_export_path(export_path, app)
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(Colors.FAIL + f"Failed to connect to {source.capitalize()}! Please check if it's running and accessible." + Colors.ENDC)
|
||||
# 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)
|
||||
|
||||
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)
|
||||
all_custom_formats.append(custom_format)
|
||||
successful_exports += 1 # Increment the counter if the export was successful
|
||||
|
||||
base_url, api_key = get_app_config(source)
|
||||
headers = {"X-Api-Key": api_key}
|
||||
params = {"apikey": api_key}
|
||||
# Hardcode the file name as 'Custom Formats (Radarr).json'
|
||||
file_name = f"Custom Formats ({app.capitalize()} - {instance['name']}).json"
|
||||
|
||||
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)
|
||||
# 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'
|
||||
|
||||
if response.status_code == 200:
|
||||
quality_profiles = response.json()
|
||||
print(Colors.OKGREEN + f"Found {len(quality_profiles)} quality profiles." + Colors.ENDC)
|
||||
print_message(f"Exported {successful_exports} custom formats to {dir_path} for {instance['name']}", 'yellow')
|
||||
print()
|
||||
|
||||
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
|
||||
def create_quality_profiles_export_path(app, config):
|
||||
# Get the export path from the config
|
||||
export_path = config['settings']['export_path']
|
||||
|
||||
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)
|
||||
# Create a directory path for the export
|
||||
dir_path = os.path.join(export_path, 'quality_profiles', app)
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(Colors.FAIL + f"Failed to connect to {source.capitalize()}! Please check if it's running and accessible." + Colors.ENDC)
|
||||
# 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__":
|
||||
user_choice, instance_name = get_user_choice()
|
||||
export_choice = get_export_choice()
|
||||
|
||||
if export_choice in ["1", "3"]:
|
||||
export_cf(user_choice, instance_name)
|
||||
if export_choice in ["2", "3"]:
|
||||
export_qf(user_choice, instance_name)
|
||||
main()
|
||||
131
helpers.py
Normal file
131
helpers.py
Normal file
@@ -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
|
||||
|
||||
428
importarr.py
428
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]
|
||||
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
|
||||
|
||||
print()
|
||||
print(Colors.OKBLUE + "Available files:" + Colors.ENDC)
|
||||
for i, file in enumerate(files, 1):
|
||||
print(f"{i}. {file}")
|
||||
|
||||
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 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 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.")
|
||||
|
||||
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_error(f"Failed to retrieve existing custom formats from {get_url}! (HTTP {response.status_code})")
|
||||
print(response.content.decode())
|
||||
print_message(f"Updating custom format '{format_name}'", "yellow", newline=False)
|
||||
print_message(" : FAIL", "red", newline=False)
|
||||
else:
|
||||
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
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
print_connection_error()
|
||||
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
|
||||
|
||||
print()
|
||||
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_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 import_quality_profiles(source_config, import_path='./profiles', selected_files=None, sync_mode=False):
|
||||
headers = {"X-Api-Key": source_config['api_key']}
|
||||
def process_profile(profile, base_url, api_key, custom_formats, existing_profiles):
|
||||
profile_name = profile.get('name')
|
||||
existing_profile = existing_profiles.get(profile_name)
|
||||
|
||||
try:
|
||||
cf_import_sync(source_config)
|
||||
# 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]
|
||||
|
||||
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))]
|
||||
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 not selected_files:
|
||||
return # Exit if no file is selected
|
||||
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"
|
||||
|
||||
for selected_file in selected_files:
|
||||
with open(os.path.join(import_path, selected_file), 'r') as file:
|
||||
response = make_request('put' if existing_profile else 'post', base_url, api_key, resource_type, profile)
|
||||
|
||||
# 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):
|
||||
|
||||
config = load_config()
|
||||
|
||||
cf_import_sync(instances)
|
||||
|
||||
all_profiles = get_profiles(app)
|
||||
selected_profiles_names = user_select_profiles(all_profiles)
|
||||
|
||||
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
|
||||
|
||||
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]
|
||||
process_profile(profile, base_url, api_key, custom_formats, existing_profiles)
|
||||
|
||||
print()
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
main()
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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)",
|
||||
@@ -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)",
|
||||
@@ -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)",
|
||||
@@ -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)",
|
||||
@@ -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)",
|
||||
@@ -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)",
|
||||
@@ -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)",
|
||||
@@ -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)",
|
||||
@@ -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)",
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
29
setup.py
29
setup.py
@@ -1,24 +1,23 @@
|
||||
config_content = """
|
||||
instances:
|
||||
master:
|
||||
sonarr:
|
||||
base_url: "http://localhost:8989"
|
||||
api_key: "API_KEY"
|
||||
radarr:
|
||||
radarr:
|
||||
- name: "Master"
|
||||
base_url: "http://localhost:7878"
|
||||
api_key: "API_KEY"
|
||||
extras:
|
||||
sonarr:
|
||||
- name: "4k-sonarr"
|
||||
base_url: "http://localhost:8998"
|
||||
api_key: "API_KEY"
|
||||
radarr:
|
||||
- name: "4k-radarr"
|
||||
base_url: "http://localhost:7887"
|
||||
api_key: "API_KEY"
|
||||
|
||||
- name: "4k-radarr"
|
||||
base_url: "http://localhost:7887"
|
||||
api_key: "API_KEY"
|
||||
sonarr:
|
||||
- name: "Master"
|
||||
base_url: "http://localhost:8989"
|
||||
api_key: "API_KEY"
|
||||
- name: "4k-sonarr"
|
||||
base_url: "http://localhost:8998"
|
||||
api_key: "API_KEY"
|
||||
settings:
|
||||
export_path: "./exports"
|
||||
import_path: "./imports"
|
||||
|
||||
"""
|
||||
|
||||
with open('config.yml', 'w') as file:
|
||||
|
||||
59
syncarr.py
59
syncarr.py
@@ -1,47 +1,22 @@
|
||||
import yaml
|
||||
import json
|
||||
import shutil
|
||||
import os
|
||||
import exportarr # Assuming this module contains the export functions
|
||||
import importarr # Assuming this module contains the import functions
|
||||
from exportarr import export_custom_formats, export_quality_profiles
|
||||
from importarr import import_custom_formats, import_quality_profiles
|
||||
from helpers import load_config, get_app_choice
|
||||
|
||||
def sync_data(sync_mode=False):
|
||||
# Load configuration from YAML file
|
||||
with open('config.yml', 'r') as config_file:
|
||||
config = yaml.safe_load(config_file)
|
||||
def main():
|
||||
app = get_app_choice().lower() # Convert to lowercase
|
||||
config = load_config() # Load the entire configuration
|
||||
|
||||
# Specify the temporary path where files will be saved
|
||||
temp_cf_path = './temp_directory/custom_formats'
|
||||
temp_qf_path = './temp_directory/quality_profiles'
|
||||
# Now app will be 'radarr' or 'sonarr', matching the keys in the config dictionary
|
||||
master_instance = next((inst for inst in config['instances'][app] if inst['name'] == 'Master'), None)
|
||||
extra_instances = [inst for inst in config['instances'][app] if inst['name'] != 'Master']
|
||||
|
||||
# Get user choice for app (radarr/sonarr)
|
||||
app_choice = input("Select the app you want to sync:\n1. Radarr\n2. Sonarr\nEnter your choice (1 or 2): ").strip()
|
||||
|
||||
while app_choice not in ["1", "2"]:
|
||||
print("Invalid input. Please enter 1 for Radarr or 2 for Sonarr.")
|
||||
app_choice = input("Enter your choice (1 or 2): ").strip()
|
||||
|
||||
app_choice = "radarr" if app_choice == "1" else "sonarr"
|
||||
|
||||
instance_name = "temp"
|
||||
exportarr.export_cf(app_choice, instance_name, save_path=temp_cf_path)
|
||||
exportarr.export_qf(app_choice, instance_name, save_path=temp_qf_path)
|
||||
|
||||
# Sync with each extra installation of the chosen app
|
||||
for extra_instance in config['instances'].get('extras', {}).get(app_choice, []):
|
||||
print(f"Importing to instance: {extra_instance['name']}")
|
||||
|
||||
# Import custom formats and quality profiles to each extra instance
|
||||
extra_instance['app_name'] = app_choice
|
||||
importarr.import_custom_formats(extra_instance, import_path=temp_cf_path, selected_files=None, sync_mode=sync_mode)
|
||||
importarr.import_quality_profiles(extra_instance, import_path=temp_qf_path, selected_files=None, sync_mode=sync_mode)
|
||||
|
||||
# Delete the temporary directories after the sync is complete
|
||||
temp_directory = './temp_directory'
|
||||
if os.path.exists(temp_directory):
|
||||
shutil.rmtree(temp_directory)
|
||||
print(f"Deleted temporary directory: {temp_directory}")
|
||||
if master_instance:
|
||||
export_custom_formats(app, [master_instance], config)
|
||||
export_quality_profiles(app, [master_instance], config)
|
||||
|
||||
if extra_instances:
|
||||
import_custom_formats(app, extra_instances)
|
||||
import_quality_profiles(app, extra_instances)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Set sync_mode to True to enable automatic selection of all files during import
|
||||
sync_data(sync_mode=True)
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user