mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-30 22:30:55 +01:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a82e8d31fe | ||
|
|
63aeded8b6 | ||
|
|
895adc9f25 | ||
|
|
ff79de7724 |
389
README.md
389
README.md
@@ -5,7 +5,6 @@ Profilarr is a Python-based tool designed to add import/export/sync functionalit
|
|||||||
## ⚠️ Before Continuing
|
## ⚠️ Before Continuing
|
||||||
|
|
||||||
- **This tool will overwrite any custom formats in your \*arr installation that have the same name.**
|
- **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.)
|
- **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
|
## 🛠️ Installation
|
||||||
@@ -27,75 +26,61 @@ Profilarr is a Python-based tool designed to add import/export/sync functionalit
|
|||||||
- This will create a `config.yml` file in the same directory as `setup.py`.
|
- This will create a `config.yml` file in the same directory as `setup.py`.
|
||||||
4. Open the `config.yml` file in a text editor.
|
4. Open the `config.yml` file in a text editor.
|
||||||
- Add the URL and API key to the master instances of Radarr / Sonarr.
|
- 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 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 exporting, adjust the `export_path` to your desired export location.
|
||||||
5. Save the changes.
|
- If importing non-Dictionary files, adjust the `import_path` to your desired import location.
|
||||||
|
5. Configure ANSI Color Support (Optional):
|
||||||
|
- The Profilarr scripts use ANSI colors in terminal output for better readability. By default, this feature is enabled (`ansi_colors: true`).
|
||||||
|
- **If your terminal does not properly display ANSI colors** (e.g., you see codes like `←[94m` instead of colored text), you may want to disable this feature to improve readability.
|
||||||
|
- To disable ANSI colors, find the `settings` section in your `config.yml` file and change `ansi_colors` to `false`.
|
||||||
|
```yaml
|
||||||
|
settings:
|
||||||
|
export_path: "./exports"
|
||||||
|
import_path: "./imports"
|
||||||
|
ansi_colors: false # Disable ANSI colors if your terminal doesn't support them
|
||||||
|
```
|
||||||
|
6. Save the changes to your `config.yml` file after making the necessary adjustments.
|
||||||
|
|
||||||
## 🚀 Usage
|
## 🚀 Usage
|
||||||
|
|
||||||
|
- If using Windows, use `python script.py` or `py script.py`. If on Linux, use `python3 script.py`.
|
||||||
|
|
||||||
### Importing
|
### Importing
|
||||||
|
|
||||||
1. Run `python importarr.py` in your command line interface.
|
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.
|
||||||
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.
|
|
||||||
|
|
||||||
#### 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
|
#### Example: Importing 1080p Transparent and 2160p Optimal Quality Profiles
|
||||||
PS Z:\Profilarr> py importarr.py
|
|
||||||
Available instances to import to:
|
|
||||||
1. Sonarr [Master]
|
|
||||||
2. Radarr [Master]
|
|
||||||
3. Sonarr [4k-sonarr]
|
|
||||||
4. Radarr [4k-radarr]
|
|
||||||
Enter the number of the instance to import to: 4
|
|
||||||
|
|
||||||
|
|
||||||
Choose what to import:
|
|
||||||
1. Custom Formats
|
|
||||||
2. Quality Profiles
|
|
||||||
Enter your choice (1/2): 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
|
Adding custom format 'D-Z0N3' : SUCCESS
|
||||||
PS Z:\Profilarr> py importarr.py
|
Adding custom format 'DON' : SUCCESS
|
||||||
Available instances to import to:
|
Adding custom format 'EbP' : SUCCESS
|
||||||
1. Sonarr [Master]
|
Adding custom format 'Geek' : SUCCESS
|
||||||
2. Radarr [Master]
|
Adding custom format 'TayTo' : SUCCESS
|
||||||
3. Sonarr [4k-sonarr]
|
... and 129 more.
|
||||||
4. Radarr [4k-radarr]
|
|
||||||
Enter the number of the instance to import to: 4
|
|
||||||
|
|
||||||
|
Successfully added 0 custom formats, updated 134 custom formats.
|
||||||
|
|
||||||
Choose what to import:
|
Available profiles:
|
||||||
1. Custom Formats
|
|
||||||
2. Quality Profiles
|
|
||||||
Enter your choice (1/2): 2
|
|
||||||
|
|
||||||
Available files:
|
|
||||||
1. 1080p Balanced (Radarr).json
|
1. 1080p Balanced (Radarr).json
|
||||||
2. 1080p Balanced (Single Grab) (Radarr).json
|
2. 1080p Balanced (Single Grab) (Radarr).json
|
||||||
3. 1080p h265 Balanced (Radarr).json
|
3. 1080p h265 Balanced (Radarr).json
|
||||||
@@ -107,209 +92,177 @@ Available files:
|
|||||||
9. 1080p Transparent (Single Grab) (Radarr).json
|
9. 1080p Transparent (Single Grab) (Radarr).json
|
||||||
10. 2160p Optimal (Radarr).json
|
10. 2160p Optimal (Radarr).json
|
||||||
11. 2160p Optimal (Single Grab) (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
|
Enter the numbers of the profiles you want to import separated by commas, or type 'all' to import all profiles:
|
||||||
Successfully added Quality Profile 1080p Balanced (Single Grab)
|
8,10
|
||||||
Successfully added Quality Profile 1080p h265 Balanced
|
Importing Quality Profiles to Radarr : 4k-radarr
|
||||||
Successfully added Quality Profile 1080p h265 Balanced (Single Grab)
|
|
||||||
Successfully added Quality Profile 1080p Optimal
|
Adding '1080p Transparent' quality profile : SUCCESS
|
||||||
Successfully added Quality Profile 1080p Optimal (Single Grab)
|
Adding '2160p Optimal' quality profile : SUCCESS
|
||||||
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>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Exporting
|
### Exporting
|
||||||
|
|
||||||
1. Run `python exportarr.py` in your command line interface.
|
1. Make sure the export path is set to your desired export location. The default is `./exports`.
|
||||||
2. Choose the instance you want to export from.
|
2. Run `python exportarr.py` in your command line interface.
|
||||||
3. Choose the data you want to export.
|
3. Follow the on-screen prompts to select your desired app and which instance(s) to export from.
|
||||||
4. The data will be exported to `exports/{instance_type}/{instance_name}/{data_type}`.
|
4. Choose the data you want to export.
|
||||||
|
5. The data will be exported to `exports/{data_type}/{app}/`.
|
||||||
|
|
||||||
#### Example
|
#### Example
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
PS Z:\Profilarr> py exportarr.py
|
Select your app of choice
|
||||||
Available sources to export from:
|
1. Radarr
|
||||||
1. Sonarr [Master]
|
2. Sonarr
|
||||||
2. Radarr [Master]
|
Enter your choice:
|
||||||
3. Sonarr [4k-sonarr]
|
1
|
||||||
4. Radarr [4k-radarr]
|
Select your Radarr instance
|
||||||
Enter the number of the app to export from: 2
|
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:
|
Exporting Custom Formats for Radarr : 4k-radarr
|
||||||
1. Custom Formats
|
Exported 134 custom formats to ./exports/custom_formats/Radarr for 4k-radarr
|
||||||
2. Quality Profiles
|
|
||||||
3. Both
|
|
||||||
Enter your choice (1/2/3): 3
|
|
||||||
|
|
||||||
Attempting to access Radarr at http://localhost:7878
|
Exporting Quality Profiles for Radarr : 4k-radarr...
|
||||||
Found 131 custom formats.
|
Exported 2 quality profiles to ./exports/quality_profiles/Radarr for 4k-radarr
|
||||||
- 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>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Syncing
|
### 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.
|
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`.
|
1. 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.
|
|
||||||
|
|
||||||
#### Example
|
#### Example
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
PS Z:\Profilarr> py syncarr.py
|
PS Z:\Profilarr> py syncarr.py
|
||||||
Select the app you want to sync:
|
Select your app of choice
|
||||||
1. Radarr
|
1. Radarr
|
||||||
2. Sonarr
|
2. Sonarr
|
||||||
Enter your choice (1 or 2): 2
|
Enter your choice:
|
||||||
Attempting to access Sonarr at http://localhost:8989
|
1
|
||||||
Found 135 custom formats.
|
Exporting Custom Formats for radarr : Master
|
||||||
- D-Z0N3
|
Exported 134 custom formats to ./exports\custom_formats\radarr for Master
|
||||||
- DON
|
|
||||||
- EbP
|
|
||||||
- Geek
|
|
||||||
- TayTo
|
|
||||||
- ZQ
|
|
||||||
- VietHD
|
|
||||||
- CtrlHD
|
|
||||||
- HiFi
|
|
||||||
- FoRM
|
|
||||||
... and 125 more.
|
|
||||||
Saved to './temp_directory/custom_formats\Custom Formats (Sonarr).json'
|
|
||||||
|
|
||||||
Attempting to access Sonarr at http://localhost:8989
|
Exporting Quality Profiles for radarr : Master...
|
||||||
Found 11 quality profiles.
|
Exported 14 quality profiles to ./exports\quality_profiles\radarr for Master
|
||||||
- 1080p Transparent
|
|
||||||
- 2160p Optimal
|
|
||||||
- 1080p Transparent (Single Grab)
|
|
||||||
- 1080p Transparent (Double Grab)
|
|
||||||
- 1080p Balanced
|
|
||||||
- 1080p Balanced (Single Grab)
|
|
||||||
- 1080p h265 Balanced
|
|
||||||
- 1080p h265 Balanced (Single Grab)
|
|
||||||
- 1080p Optimal
|
|
||||||
- 1080p Optimal (Single Grab)
|
|
||||||
... and 1 more.
|
|
||||||
Saved to 'temp_directory\quality_profiles'
|
|
||||||
|
|
||||||
Importing to instance: 4k-sonarr
|
Importing custom formats to radarr : 4k-radarr
|
||||||
Adding custom format 'D-Z0N3': SUCCESS
|
|
||||||
Adding custom format 'DON': SUCCESS
|
|
||||||
Adding custom format 'EbP': SUCCESS
|
|
||||||
Adding custom format 'Geek': SUCCESS
|
|
||||||
Adding custom format 'TayTo': SUCCESS
|
|
||||||
Adding custom format 'ZQ': SUCCESS
|
|
||||||
Adding custom format 'VietHD': SUCCESS
|
|
||||||
Adding custom format 'CtrlHD': SUCCESS
|
|
||||||
Adding custom format 'HiFi': SUCCESS
|
|
||||||
... and 125 more.
|
|
||||||
|
|
||||||
Successfully added 135 custom formats, updated 0 custom formats.
|
...
|
||||||
Successfully added Quality Profile 1080p Balanced (Single Grab)
|
Updating custom format 'Blu-Ray (Remux)' : SUCCESS
|
||||||
Successfully added Quality Profile 1080p Balanced
|
Updating custom format 'MAX' : SUCCESS
|
||||||
Successfully added Quality Profile 1080p h265 Balanced
|
Updating custom format 'h265 (4k)' : SUCCESS
|
||||||
Successfully added Quality Profile 1080p h265 Balanced (Single Grab)
|
Updating custom format 'TEST FLAC' : SUCCESS
|
||||||
Successfully added Quality Profile 1080p Optimal (Single Grab)
|
|
||||||
Successfully added Quality Profile 1080p Optimal
|
Successfully added 134 custom formats, updated 0 custom formats.
|
||||||
Successfully added Quality Profile 1080p Transparent (Double Grab)
|
|
||||||
Successfully added Quality Profile 1080p Transparent (Single Grab)
|
Available profiles:
|
||||||
Successfully added Quality Profile 1080p Transparent
|
1. 1080p Balanced (Radarr).json
|
||||||
Successfully added Quality Profile 2160p Optimal (Single Grab)
|
2. 1080p Balanced (Single Grab) (Radarr).json
|
||||||
Successfully added Quality Profile 2160p Optimal
|
3. 1080p h265 Balanced (Radarr).json
|
||||||
Deleted temporary directory: ./temp_directory
|
4. 1080p h265 Balanced (Single Grab) (Radarr).json
|
||||||
PS Z:\Profilarr>
|
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
|
### Deleting
|
||||||
|
|
||||||
1. Run `python deletarr.py` in your command line interface.
|
1. Run `python deletarr.py` in your command line interface.
|
||||||
2. Select the instance from which you wish to delete data.
|
2. Select the instance(s) from which you wish to delete data.
|
||||||
3. Choose between deleting Custom Formats or Quality Profiles.
|
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.
|
4. Select specific items by typing their numbers separated by commas, or type 'all' to delete everything.
|
||||||
|
|
||||||
#### Example: Deleting Custom Formats
|
#### Example
|
||||||
|
|
||||||
```plaintext
|
```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:
|
Please select what you want to delete:
|
||||||
1. Sonarr [Master]
|
|
||||||
2. Radarr [Master]
|
|
||||||
Enter the number of the instance to delete from: 2
|
|
||||||
|
|
||||||
Choose what to delete:
|
|
||||||
1. Custom Formats
|
1. Custom Formats
|
||||||
2. Quality Profiles
|
2. Quality Profiles
|
||||||
Enter your choice (1/2): 1
|
3. Both
|
||||||
|
Enter your choice: 3
|
||||||
Deleting selected custom formats...
|
Available items to delete:
|
||||||
|
1. D-Z0N3
|
||||||
Available items:
|
2. DON
|
||||||
1. UHDBits
|
3. EbP
|
||||||
2. Dolby Vision w/out Fallback
|
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
|
Your choice: all
|
||||||
|
|
||||||
Deleting custom format 'UHDBits': SUCCESS
|
Deleting Quality Profile (1080p Transparent) : SUCCESS
|
||||||
...
|
Deleting Quality Profile (2160p Optimal) : SUCCESS
|
||||||
Deleting custom format 'MAX': SUCCESS
|
Deleting Quality Profile (1080p Balanced) : SUCCESS
|
||||||
```
|
Deleting Quality Profile (1080p Balanced (Single Grab)) : SUCCESS
|
||||||
|
Deleting Quality Profile (1080p h265 Balanced) : SUCCESS
|
||||||
#### Example: Deleting Quality Profiles
|
Deleting Quality Profile (1080p h265 Balanced (Single Grab)) : SUCCESS
|
||||||
|
Deleting Quality Profile (1080p Optimal) : SUCCESS
|
||||||
```plaintext
|
Deleting Quality Profile (1080p Optimal (Single Grab)) : SUCCESS
|
||||||
PS Z:\Profilarr> python deletarr.py
|
Deleting Quality Profile (1080p Transparent (Double Grab)) : SUCCESS
|
||||||
|
Deleting Quality Profile (1080p Transparent (Single Grab)) : SUCCESS
|
||||||
Choose what to delete:
|
Deleting Quality Profile (2160p Optimal (Single Grab)) : SUCCESS
|
||||||
1. Custom Formats
|
PS Z:\Profilarr>
|
||||||
2. Quality Profiles
|
|
||||||
Enter your choice (1/2): 2
|
|
||||||
|
|
||||||
Deleting selected quality profiles...
|
|
||||||
|
|
||||||
Available items:
|
|
||||||
1. 1080p Balanced
|
|
||||||
...
|
|
||||||
11. 2160p Optimal
|
|
||||||
Your choice: all
|
|
||||||
|
|
||||||
Deleting quality profile '1080p Balanced': SUCCESS
|
|
||||||
...
|
|
||||||
Deleting quality profile '2160p Optimal': SUCCESS
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Radarr and Sonarr Compatibility
|
### Radarr and Sonarr Compatibility
|
||||||
|
|||||||
239
deletarr.py
239
deletarr.py
@@ -1,164 +1,101 @@
|
|||||||
import requests
|
from helpers import *
|
||||||
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
|
|
||||||
|
|
||||||
def user_select_items_to_delete(items):
|
def user_select_items_to_delete(items):
|
||||||
print(Colors.HEADER + "\nAvailable items:" + Colors.ENDC)
|
"""
|
||||||
for idx, item in enumerate(items, start=1):
|
Prompts the user to select items to delete from a given list of items.
|
||||||
print(f"{idx}. {item['name']}")
|
Each item in the list is expected to be a dictionary with at least an 'id' and 'name' key.
|
||||||
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)
|
"""
|
||||||
|
print_message("Available items to delete:", "purple")
|
||||||
|
for index, item in enumerate(items, start=1):
|
||||||
|
print_message(f"{index}. {item['name']}", "green")
|
||||||
|
|
||||||
selection = input(Colors.HEADER + "Your choice: " + Colors.ENDC).strip().lower()
|
print_message("Enter the number(s) of the items you wish to delete, separated by commas, or type 'all' for all:", "yellow")
|
||||||
if selection == 'all':
|
user_input = input("Your choice: ").strip().lower()
|
||||||
return [item['id'] for item in items] # Return all IDs if "all" is selected
|
selected_items = []
|
||||||
|
|
||||||
|
if user_input == 'all':
|
||||||
|
return items
|
||||||
else:
|
else:
|
||||||
selected_ids = []
|
indices = user_input.split(',')
|
||||||
try:
|
for index in indices:
|
||||||
selected_indices = [int(i) - 1 for i in selection.split(',') if i.isdigit()]
|
try:
|
||||||
for idx in selected_indices:
|
index = int(index.strip()) - 1
|
||||||
if idx < len(items):
|
if 0 <= index < len(items):
|
||||||
selected_ids.append(items[idx]['id'])
|
selected_items.append(items[index])
|
||||||
return selected_ids
|
|
||||||
except ValueError:
|
|
||||||
print_error("Invalid input. Please enter a valid number or 'all'.")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def delete_custom_formats(source_config):
|
|
||||||
print(Colors.OKBLUE + "\nDeleting selected custom formats..." + Colors.ENDC)
|
|
||||||
headers = {"X-Api-Key": source_config['api_key']}
|
|
||||||
get_url = f"{source_config['base_url']}/api/v3/customformat"
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.get(get_url, headers=headers)
|
|
||||||
if response.status_code == 200:
|
|
||||||
formats_to_delete = response.json()
|
|
||||||
selected_ids = user_select_items_to_delete(formats_to_delete)
|
|
||||||
|
|
||||||
for format_id in selected_ids:
|
|
||||||
delete_url = f"{get_url}/{format_id}"
|
|
||||||
del_response = requests.delete(delete_url, headers=headers)
|
|
||||||
format_name = next((item['name'] for item in formats_to_delete if item['id'] == format_id), "Unknown")
|
|
||||||
if del_response.status_code in [200, 202, 204]:
|
|
||||||
print(Colors.OKBLUE + f"Deleting custom format '{format_name}': " + Colors.ENDC + Colors.OKGREEN + "SUCCESS" + Colors.ENDC)
|
|
||||||
else:
|
else:
|
||||||
print(Colors.OKBLUE + f"Deleting custom format '{format_name}': " + Colors.ENDC + Colors.FAIL + "FAIL" + Colors.ENDC)
|
print_message("Invalid selection, ignoring.", "red")
|
||||||
else:
|
except ValueError:
|
||||||
print_error("Failed to retrieve custom formats for deletion!")
|
print_message("Invalid input, please enter numbers only.", "red")
|
||||||
except requests.exceptions.ConnectionError:
|
|
||||||
print_connection_error()
|
|
||||||
|
|
||||||
def delete_quality_profiles(source_config):
|
return selected_items
|
||||||
print(Colors.OKBLUE + "\nDeleting selected quality profiles..." + Colors.ENDC)
|
|
||||||
headers = {"X-Api-Key": source_config['api_key']}
|
|
||||||
get_url = f"{source_config['base_url']}/api/v3/qualityprofile"
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.get(get_url, headers=headers)
|
|
||||||
if response.status_code == 200:
|
|
||||||
profiles_to_delete = response.json()
|
|
||||||
selected_ids = user_select_items_to_delete(profiles_to_delete)
|
|
||||||
|
|
||||||
for profile_id in selected_ids:
|
def prompt_export_choice():
|
||||||
delete_url = f"{get_url}/{profile_id}"
|
"""
|
||||||
del_response = requests.delete(delete_url, headers=headers)
|
Prompt user to choose between exporting Custom Formats, Quality Profiles, or both.
|
||||||
profile_name = next((item['name'] for item in profiles_to_delete if item['id'] == profile_id), "Unknown")
|
Returns a list of choices.
|
||||||
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)
|
print_message("Please select what you want to delete:", "blue")
|
||||||
else:
|
options = {"1": "Custom Formats", "2": "Quality Profiles", "3": "Both"}
|
||||||
# Handle failure due to the profile being in use or other errors
|
for key, value in options.items():
|
||||||
error_message = "Failed to delete due to an unknown error."
|
print_message(f"{key}. {value}", "green")
|
||||||
try:
|
choice = input("Enter your choice: ").strip()
|
||||||
# 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)
|
# Validate choice
|
||||||
else:
|
while choice not in options:
|
||||||
print_error("Failed to retrieve quality profiles for deletion!")
|
print_message("Invalid choice, please select a valid option:", "red")
|
||||||
except requests.exceptions.ConnectionError:
|
choice = input("Enter your choice: ").strip()
|
||||||
print_connection_error()
|
|
||||||
|
|
||||||
def get_app_config(app_name, instance_name):
|
if choice == "3":
|
||||||
if instance_name.endswith("[Master]"):
|
return ["Custom Formats", "Quality Profiles"]
|
||||||
return master_config[app_name]
|
|
||||||
else:
|
else:
|
||||||
instance_name = instance_name.replace(f"{app_name.capitalize()} [", "").replace("]", "")
|
return [options[choice]]
|
||||||
extras = config['instances']['extras'].get(app_name, [])
|
|
||||||
for instance in extras:
|
def delete_custom_formats_or_profiles(app, instance, item_type, config):
|
||||||
if instance['name'] == instance_name:
|
"""
|
||||||
return instance
|
Deletes either custom formats or quality profiles based on the item_type.
|
||||||
raise ValueError(f"Configuration for {app_name} - {instance_name} not found.")
|
"""
|
||||||
|
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__":
|
if __name__ == "__main__":
|
||||||
selected_app, selected_instance = get_user_choice()
|
main()
|
||||||
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)
|
|
||||||
|
|||||||
@@ -1,23 +1,37 @@
|
|||||||
version: "3.3"
|
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:
|
services:
|
||||||
radarr:
|
radarr:
|
||||||
|
<<: *common-settings
|
||||||
image: linuxserver/radarr
|
image: linuxserver/radarr
|
||||||
container_name: 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:
|
ports:
|
||||||
- "7887:7878" # change the left value to the desired host port for Radarr
|
- "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:
|
sonarr:
|
||||||
|
<<: *common-settings
|
||||||
image: linuxserver/sonarr
|
image: linuxserver/sonarr
|
||||||
container_name: 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:
|
ports:
|
||||||
- "8998:8989" # change the left value to the desired host port for Sonarr
|
- "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
|
||||||
|
|||||||
269
exportarr.py
269
exportarr.py
@@ -1,183 +1,134 @@
|
|||||||
import requests
|
from helpers import *
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import yaml
|
|
||||||
import json
|
|
||||||
|
|
||||||
# ANSI escape sequences for colors
|
def prompt_export_choice():
|
||||||
class Colors:
|
options = { "1": "Custom Formats", "2": "Quality Profiles" }
|
||||||
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
|
print_message("Please select what you want to export:", "blue")
|
||||||
with open('config.yml', 'r') as config_file:
|
for number, option in options.items():
|
||||||
config = yaml.safe_load(config_file)
|
print_message(f"{number}. {option}", "green")
|
||||||
master_config = config['instances']['master']
|
print_message("Enter the number(s) of your choice, multiple separated by commas, or type 'all' for all options", "yellow")
|
||||||
export_base_path = config['settings']['export_path']
|
|
||||||
|
|
||||||
def get_user_choice():
|
user_choice = input("Your choice: ")
|
||||||
sources = []
|
|
||||||
print(Colors.HEADER + "Available sources to export from:" + Colors.ENDC)
|
|
||||||
|
|
||||||
# Add master installations
|
if user_choice.lower() == 'all':
|
||||||
for app in master_config:
|
return list(options.values())
|
||||||
sources.append((app, f"{app.capitalize()} [Master]", "master"))
|
|
||||||
|
|
||||||
# Add extra installations
|
|
||||||
if "extras" in config['instances']:
|
|
||||||
for app, instances in config['instances']['extras'].items():
|
|
||||||
for install in instances:
|
|
||||||
sources.append((app, f"{app.capitalize()} [{install['name']}]", install['name']))
|
|
||||||
|
|
||||||
# Display sources with numbers
|
|
||||||
for idx, (app, name, _) in enumerate(sources, start=1):
|
|
||||||
print(f"{idx}. {name}")
|
|
||||||
|
|
||||||
# User selection
|
|
||||||
choice = input("Enter the number of the app to export from: ").strip()
|
|
||||||
while not choice.isdigit() or int(choice) < 1 or int(choice) > len(sources):
|
|
||||||
print(Colors.FAIL + "Invalid input. Please enter a valid number." + Colors.ENDC)
|
|
||||||
choice = input("Enter the number of the app to export from: ").strip()
|
|
||||||
|
|
||||||
selected_app, instance_name = sources[int(choice) - 1][0], sources[int(choice) - 1][2]
|
|
||||||
print()
|
|
||||||
return selected_app, instance_name
|
|
||||||
|
|
||||||
|
|
||||||
def get_export_choice():
|
|
||||||
print(Colors.HEADER + "Choose what to export:" + Colors.ENDC)
|
|
||||||
print("1. Custom Formats")
|
|
||||||
print("2. Quality Profiles")
|
|
||||||
print("3. Both")
|
|
||||||
choice = input("Enter your choice (1/2/3): ").strip()
|
|
||||||
while choice not in ["1", "2", "3"]:
|
|
||||||
print(Colors.FAIL + "Invalid input. Please enter 1, 2, or 3." + Colors.ENDC)
|
|
||||||
choice = input("Enter your choice (1/2/3): ").strip()
|
|
||||||
print()
|
|
||||||
return choice
|
|
||||||
|
|
||||||
def get_app_config(source):
|
|
||||||
app_config = master_config[source]
|
|
||||||
return app_config['base_url'], app_config['api_key']
|
|
||||||
|
|
||||||
def sanitize_filename(filename):
|
|
||||||
sanitized_filename = re.sub(r'[\\/*?:"<>|]', '_', filename)
|
|
||||||
return sanitized_filename
|
|
||||||
|
|
||||||
def handle_response_errors(response):
|
|
||||||
if response.status_code == 401:
|
|
||||||
print(Colors.FAIL + "Authentication error: Invalid API key." + Colors.ENDC)
|
|
||||||
elif response.status_code == 403:
|
|
||||||
print(Colors.FAIL + "Forbidden: Access is denied." + Colors.ENDC)
|
|
||||||
else:
|
else:
|
||||||
print(Colors.FAIL + f"An error occurred! (HTTP {response.status_code})" + Colors.ENDC)
|
return [options[choice] for choice in user_choice.split(',') if choice in options]
|
||||||
print("Response Content: ", response.content.decode('utf-8'))
|
|
||||||
|
|
||||||
def print_saved_items(items, item_type):
|
def create_export_path(export_path, app):
|
||||||
if len(items) > 10:
|
# Convert app to lowercase
|
||||||
items_to_display = items[:10]
|
app = app.lower() # Ensure app is in lowercase
|
||||||
for item in items_to_display:
|
# Create a directory path for the export in lowercase
|
||||||
print(f" - {item}")
|
dir_path = os.path.join(export_path, 'custom_formats', app).lower() # Convert entire path to lowercase
|
||||||
print(f"... and {len(items) - 10} more.")
|
|
||||||
else:
|
|
||||||
for item in items:
|
|
||||||
print(f" - {item}")
|
|
||||||
|
|
||||||
def ensure_directory_exists(directory):
|
# Create the directory if it doesn't exist
|
||||||
if not os.path.exists(directory):
|
os.makedirs(dir_path, exist_ok=True)
|
||||||
os.makedirs(directory)
|
|
||||||
print(Colors.OKBLUE + f"Created directory: {directory}" + Colors.ENDC)
|
|
||||||
|
|
||||||
def export_cf(source, instance_name, save_path=None):
|
return dir_path
|
||||||
if save_path is None:
|
|
||||||
save_path = os.path.join(export_base_path, source, instance_name, 'custom_formats')
|
|
||||||
ensure_directory_exists(save_path)
|
|
||||||
|
|
||||||
base_url, api_key = get_app_config(source)
|
def export_custom_formats(app, instances, config):
|
||||||
headers = {"X-Api-Key": api_key}
|
|
||||||
params = {"apikey": api_key}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
data = response.json()
|
|
||||||
print(Colors.OKGREEN + f"Found {len(data)} custom formats." + Colors.ENDC)
|
|
||||||
|
|
||||||
saved_formats = []
|
|
||||||
for custom_format in data:
|
|
||||||
custom_format.pop('id', None)
|
|
||||||
saved_formats.append(custom_format['name'])
|
|
||||||
|
|
||||||
file_path = os.path.join(save_path, f'Custom Formats ({source.capitalize()}).json')
|
|
||||||
with open(file_path, 'w') as f:
|
|
||||||
json.dump(data, f, indent=4)
|
|
||||||
|
|
||||||
print_saved_items(saved_formats, "Custom Formats")
|
|
||||||
print(Colors.OKGREEN + f"Saved to '{file_path}'" + Colors.ENDC)
|
|
||||||
print()
|
|
||||||
else:
|
|
||||||
handle_response_errors(response)
|
|
||||||
|
|
||||||
except requests.exceptions.ConnectionError:
|
|
||||||
print(Colors.FAIL + f"Failed to connect to {source.capitalize()}! Please check if it's running and accessible." + Colors.ENDC)
|
|
||||||
|
|
||||||
|
|
||||||
|
for instance in instances:
|
||||||
|
print_message(f"Exporting Custom Formats for {app.capitalize()} : {instance['name']}", 'blue')
|
||||||
|
|
||||||
def export_qf(source, instance_name, save_path=None):
|
url = instance['base_url']
|
||||||
if save_path is None:
|
api_key = instance['api_key']
|
||||||
save_path = os.path.join(export_base_path, source, instance_name, 'profiles')
|
|
||||||
ensure_directory_exists(save_path)
|
|
||||||
|
|
||||||
base_url, api_key = get_app_config(source)
|
# Get the export path from the config
|
||||||
headers = {"X-Api-Key": api_key}
|
export_path = config['settings']['export_path']
|
||||||
params = {"apikey": api_key}
|
|
||||||
|
|
||||||
print(Colors.OKBLUE + f"Attempting to access {source.capitalize()} at {base_url}" + Colors.ENDC)
|
# Create the export directory
|
||||||
|
dir_path = create_export_path(export_path, app)
|
||||||
|
|
||||||
try:
|
# Assuming 'export' is a valid resource_type for the API
|
||||||
response = requests.get(f"{base_url}/api/v3/qualityprofile", params=params, headers=headers)
|
response = make_request('get', url, api_key, 'customformat')
|
||||||
|
|
||||||
if response.status_code == 200:
|
successful_exports = 0 # Counter for successful exports
|
||||||
quality_profiles = response.json()
|
|
||||||
print(Colors.OKGREEN + f"Found {len(quality_profiles)} quality profiles." + Colors.ENDC)
|
|
||||||
|
|
||||||
saved_profiles = []
|
# Scrub the JSON data and save each custom format in its own file
|
||||||
for profile in quality_profiles:
|
all_custom_formats = []
|
||||||
profile.pop('id', None)
|
for custom_format in response:
|
||||||
profile_name = profile.get('name', 'unnamed_profile')
|
# Remove the 'id' field
|
||||||
profile_name = sanitize_filename(profile_name)
|
custom_format.pop('id', None)
|
||||||
profile_filename = f"{profile_name} ({source.capitalize()}).json"
|
|
||||||
profile_filepath = os.path.join(save_path, profile_filename)
|
|
||||||
with open(profile_filepath, 'w') as file:
|
|
||||||
json.dump([profile], file, indent=4)
|
|
||||||
saved_profiles.append(profile_name) # Add the profile name to the list
|
|
||||||
|
|
||||||
print_saved_items(saved_profiles, "Quality Profiles")
|
all_custom_formats.append(custom_format)
|
||||||
print(Colors.OKGREEN + f"Saved to '{os.path.normpath(save_path)}'" + Colors.ENDC) # Normalize the path
|
successful_exports += 1 # Increment the counter if the export was successful
|
||||||
print()
|
|
||||||
else:
|
|
||||||
handle_response_errors(response)
|
|
||||||
|
|
||||||
except requests.exceptions.ConnectionError:
|
file_name = f"custom formats ({app.lower()} - {instance['name'].lower()}).json"
|
||||||
print(Colors.FAIL + f"Failed to connect to {source.capitalize()}! Please check if it's running and accessible." + Colors.ENDC)
|
|
||||||
|
|
||||||
|
# Save all custom formats to a single file in the export directory
|
||||||
|
try:
|
||||||
|
with open(os.path.join(dir_path, file_name), 'w') as f:
|
||||||
|
json.dump(all_custom_formats, f, indent=4)
|
||||||
|
status = 'SUCCESS'
|
||||||
|
status_color = 'green'
|
||||||
|
except Exception as e:
|
||||||
|
status = 'FAILED'
|
||||||
|
status_color = 'red'
|
||||||
|
|
||||||
|
print_message(f"Exported {successful_exports} custom formats to {dir_path} for {instance['name']}", 'yellow')
|
||||||
|
print()
|
||||||
|
|
||||||
|
def create_quality_profiles_export_path(app, config):
|
||||||
|
# Get the export path from the config
|
||||||
|
export_path = config['settings']['export_path']
|
||||||
|
|
||||||
|
# Create a directory path for the export
|
||||||
|
dir_path = os.path.join(export_path, 'quality_profiles', app)
|
||||||
|
|
||||||
|
# Create the directory if it doesn't exist
|
||||||
|
os.makedirs(dir_path, exist_ok=True)
|
||||||
|
|
||||||
|
return dir_path
|
||||||
|
|
||||||
|
def export_quality_profiles(app, instances, config):
|
||||||
|
for instance in instances:
|
||||||
|
print_message(f"Exporting Quality Profiles for {app.capitalize()} : {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.lower()} - {instance['name'].lower()}).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__":
|
if __name__ == "__main__":
|
||||||
user_choice, instance_name = get_user_choice()
|
main()
|
||||||
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)
|
|
||||||
151
helpers.py
Normal file
151
helpers.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import yaml
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
from requests.exceptions import ConnectionError, Timeout, TooManyRedirects
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
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):
|
||||||
|
config = load_config()
|
||||||
|
ansi_colors = config['settings']['ansi_colors']
|
||||||
|
|
||||||
|
if ansi_colors:
|
||||||
|
# Initialize color as default.
|
||||||
|
color = Colors.ENDC
|
||||||
|
message_type = message_type.lower()
|
||||||
|
|
||||||
|
# Assign color based on message type.
|
||||||
|
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
|
||||||
|
|
||||||
|
# Prepare the end color reset code.
|
||||||
|
end_color = Colors.ENDC
|
||||||
|
|
||||||
|
# Print the colored message.
|
||||||
|
if newline:
|
||||||
|
print(color + message + end_color)
|
||||||
|
else:
|
||||||
|
print(color + message + end_color, end='')
|
||||||
|
else:
|
||||||
|
# Print the message without color if ANSI colors are disabled.
|
||||||
|
if newline:
|
||||||
|
print(message)
|
||||||
|
else:
|
||||||
|
print(message, 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")
|
||||||
|
sys.exit()
|
||||||
|
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")
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
426
importarr.py
426
importarr.py
@@ -1,265 +1,211 @@
|
|||||||
import requests
|
from helpers import *
|
||||||
import os
|
import os
|
||||||
import re
|
import fnmatch
|
||||||
import yaml
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
# ANSI escape sequences for colors
|
def get_custom_formats(app):
|
||||||
class Colors:
|
config = load_config()
|
||||||
HEADER = '\033[95m'
|
import_path = f"{config['settings']['import_path']}/custom_formats/{app.lower()}" # Adjusted path
|
||||||
OKBLUE = '\033[94m'
|
for file in os.listdir(import_path):
|
||||||
OKGREEN = '\033[92m'
|
if fnmatch.fnmatch(file, f'*{app}*'):
|
||||||
WARNING = '\033[93m'
|
return file
|
||||||
FAIL = '\033[91m'
|
return None
|
||||||
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 process_format(format, existing_names_to_id, base_url, api_key):
|
||||||
def import_custom_formats(source_config, import_path='./custom_formats', selected_files=None, sync_mode=False):
|
format_name = format['name']
|
||||||
headers = {"X-Api-Key": source_config['api_key']}
|
if format_name in existing_names_to_id:
|
||||||
get_url = f"{source_config['base_url']}/api/v3/customformat"
|
format_id = existing_names_to_id[format_name]
|
||||||
|
response = make_request('put', base_url, api_key, f'customformat/{format_id}', format)
|
||||||
try:
|
if response is not None:
|
||||||
response = requests.get(get_url, headers=headers)
|
print_message(f"Updating custom format '{format_name}'", "yellow", newline=False)
|
||||||
if response.status_code == 200:
|
print_message(" : SUCCESS", "green")
|
||||||
existing_formats = response.json()
|
return 0, 1
|
||||||
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.")
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print_error(f"Failed to retrieve existing custom formats from {get_url}! (HTTP {response.status_code})")
|
print_message(f"Updating custom format '{format_name}'", "yellow", newline=False)
|
||||||
print(response.content.decode())
|
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 1, 0
|
||||||
|
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:
|
def import_custom_formats(app, instances):
|
||||||
print_connection_error()
|
|
||||||
|
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.lower()}/{app_file}", 'r') as import_file:
|
||||||
|
import_formats = json.load(import_file)
|
||||||
|
|
||||||
|
print_message(f"Importing custom formats to {app.capitalize()} : {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):
|
def process_profile(profile, base_url, api_key, custom_formats, existing_profiles):
|
||||||
headers = {"X-Api-Key": source_config['api_key']}
|
profile_name = profile.get('name')
|
||||||
|
existing_profile = existing_profiles.get(profile_name)
|
||||||
|
|
||||||
try:
|
# Update or add custom format items as needed
|
||||||
cf_import_sync(source_config)
|
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:
|
for format_name, format_id in custom_formats.items():
|
||||||
if sync_mode:
|
if format_name not in {item.get('name') for item in profile.get('formatItems', [])}:
|
||||||
# Automatically select all profile files
|
profile.setdefault('formatItems', []).append({
|
||||||
selected_files = [f for f in os.listdir(import_path) if os.path.isfile(os.path.join(import_path, f))]
|
"format": format_id,
|
||||||
|
"name": format_name,
|
||||||
|
"score": 0
|
||||||
|
})
|
||||||
|
|
||||||
if not selected_files:
|
if existing_profile:
|
||||||
return # Exit if no file is selected
|
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:
|
response = make_request('put' if existing_profile else 'post', base_url, api_key, resource_type, profile)
|
||||||
with open(os.path.join(import_path, selected_file), 'r') as file:
|
|
||||||
|
# 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.lower()}/{profile_file}", 'r') as file:
|
||||||
try:
|
try:
|
||||||
quality_profiles = json.load(file)
|
quality_profiles = json.load(file)
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
print_error(f"Error loading selected profile: {e}")
|
print_message(f"Error loading selected profile: {e}", "red")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for profile in quality_profiles:
|
for profile in quality_profiles:
|
||||||
existing_format_names = set()
|
process_profile(profile, base_url, api_key, custom_formats, existing_profiles)
|
||||||
if 'formatItems' in profile:
|
|
||||||
for format_item in profile['formatItems']:
|
|
||||||
format_name = format_item.get('name')
|
|
||||||
if format_name:
|
|
||||||
existing_format_names.add(format_name)
|
|
||||||
if format_name in source_config['custom_formats']:
|
|
||||||
format_item['format'] = source_config['custom_formats'][format_name]
|
|
||||||
|
|
||||||
for format_name, format_id in source_config['custom_formats'].items():
|
print()
|
||||||
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"
|
def main():
|
||||||
response = requests.post(post_url, json=profile, headers=headers)
|
app = get_app_choice()
|
||||||
|
instances = get_instance_choice(app)
|
||||||
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)
|
|
||||||
|
|
||||||
|
import_custom_formats(app, instances)
|
||||||
|
import_quality_profiles(app, instances)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
selected_app, selected_instance = get_user_choice()
|
main()
|
||||||
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)
|
|
||||||
@@ -8949,6 +8949,130 @@
|
|||||||
"isFloat": false
|
"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,387 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Freeleech100",
|
||||||
|
"includeCustomFormatWhenRenaming": false,
|
||||||
|
"specifications": [
|
||||||
|
{
|
||||||
|
"name": "Freeleech100",
|
||||||
|
"implementation": "IndexerFlagSpecification",
|
||||||
|
"implementationName": "Indexer Flag",
|
||||||
|
"infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2",
|
||||||
|
"negate": false,
|
||||||
|
"required": true,
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"order": 0,
|
||||||
|
"name": "value",
|
||||||
|
"label": "Flag",
|
||||||
|
"value": 1,
|
||||||
|
"type": "select",
|
||||||
|
"advanced": false,
|
||||||
|
"selectOptions": [
|
||||||
|
{
|
||||||
|
"value": 1,
|
||||||
|
"name": "G Freeleech",
|
||||||
|
"order": 1,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 2,
|
||||||
|
"name": "G Halfleech",
|
||||||
|
"order": 2,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 4,
|
||||||
|
"name": "G DoubleUpload",
|
||||||
|
"order": 4,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 8,
|
||||||
|
"name": "PTP Golden",
|
||||||
|
"order": 8,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 16,
|
||||||
|
"name": "PTP Approved",
|
||||||
|
"order": 16,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 32,
|
||||||
|
"name": "G Internal",
|
||||||
|
"order": 32,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 128,
|
||||||
|
"name": "G Scene",
|
||||||
|
"order": 128,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 256,
|
||||||
|
"name": "G Freeleech75",
|
||||||
|
"order": 256,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 512,
|
||||||
|
"name": "G Freeleech25",
|
||||||
|
"order": 512,
|
||||||
|
"dividerAfter": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"privacy": "normal",
|
||||||
|
"isFloat": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Freeleech75",
|
||||||
|
"includeCustomFormatWhenRenaming": false,
|
||||||
|
"specifications": [
|
||||||
|
{
|
||||||
|
"name": "Freeleech75",
|
||||||
|
"implementation": "IndexerFlagSpecification",
|
||||||
|
"implementationName": "Indexer Flag",
|
||||||
|
"infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2",
|
||||||
|
"negate": false,
|
||||||
|
"required": true,
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"order": 0,
|
||||||
|
"name": "value",
|
||||||
|
"label": "Flag",
|
||||||
|
"value": 256,
|
||||||
|
"type": "select",
|
||||||
|
"advanced": false,
|
||||||
|
"selectOptions": [
|
||||||
|
{
|
||||||
|
"value": 1,
|
||||||
|
"name": "G Freeleech",
|
||||||
|
"order": 1,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 2,
|
||||||
|
"name": "G Halfleech",
|
||||||
|
"order": 2,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 4,
|
||||||
|
"name": "G DoubleUpload",
|
||||||
|
"order": 4,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 8,
|
||||||
|
"name": "PTP Golden",
|
||||||
|
"order": 8,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 16,
|
||||||
|
"name": "PTP Approved",
|
||||||
|
"order": 16,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 32,
|
||||||
|
"name": "G Internal",
|
||||||
|
"order": 32,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 128,
|
||||||
|
"name": "G Scene",
|
||||||
|
"order": 128,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 256,
|
||||||
|
"name": "G Freeleech75",
|
||||||
|
"order": 256,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 512,
|
||||||
|
"name": "G Freeleech25",
|
||||||
|
"order": 512,
|
||||||
|
"dividerAfter": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"privacy": "normal",
|
||||||
|
"isFloat": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Freeleech50",
|
||||||
|
"includeCustomFormatWhenRenaming": false,
|
||||||
|
"specifications": [
|
||||||
|
{
|
||||||
|
"name": "Freeleech50",
|
||||||
|
"implementation": "IndexerFlagSpecification",
|
||||||
|
"implementationName": "Indexer Flag",
|
||||||
|
"infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2",
|
||||||
|
"negate": false,
|
||||||
|
"required": true,
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"order": 0,
|
||||||
|
"name": "value",
|
||||||
|
"label": "Flag",
|
||||||
|
"value": 2,
|
||||||
|
"type": "select",
|
||||||
|
"advanced": false,
|
||||||
|
"selectOptions": [
|
||||||
|
{
|
||||||
|
"value": 1,
|
||||||
|
"name": "G Freeleech",
|
||||||
|
"order": 1,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 2,
|
||||||
|
"name": "G Halfleech",
|
||||||
|
"order": 2,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 4,
|
||||||
|
"name": "G DoubleUpload",
|
||||||
|
"order": 4,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 8,
|
||||||
|
"name": "PTP Golden",
|
||||||
|
"order": 8,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 16,
|
||||||
|
"name": "PTP Approved",
|
||||||
|
"order": 16,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 32,
|
||||||
|
"name": "G Internal",
|
||||||
|
"order": 32,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 128,
|
||||||
|
"name": "G Scene",
|
||||||
|
"order": 128,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 256,
|
||||||
|
"name": "G Freeleech75",
|
||||||
|
"order": 256,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 512,
|
||||||
|
"name": "G Freeleech25",
|
||||||
|
"order": 512,
|
||||||
|
"dividerAfter": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"privacy": "normal",
|
||||||
|
"isFloat": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Freeleech25",
|
||||||
|
"includeCustomFormatWhenRenaming": false,
|
||||||
|
"specifications": [
|
||||||
|
{
|
||||||
|
"name": "Freeleech25",
|
||||||
|
"implementation": "IndexerFlagSpecification",
|
||||||
|
"implementationName": "Indexer Flag",
|
||||||
|
"infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2",
|
||||||
|
"negate": false,
|
||||||
|
"required": true,
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"order": 0,
|
||||||
|
"name": "value",
|
||||||
|
"label": "Flag",
|
||||||
|
"value": 512,
|
||||||
|
"type": "select",
|
||||||
|
"advanced": false,
|
||||||
|
"selectOptions": [
|
||||||
|
{
|
||||||
|
"value": 1,
|
||||||
|
"name": "G Freeleech",
|
||||||
|
"order": 1,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 2,
|
||||||
|
"name": "G Halfleech",
|
||||||
|
"order": 2,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 4,
|
||||||
|
"name": "G DoubleUpload",
|
||||||
|
"order": 4,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 8,
|
||||||
|
"name": "PTP Golden",
|
||||||
|
"order": 8,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 16,
|
||||||
|
"name": "PTP Approved",
|
||||||
|
"order": 16,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 32,
|
||||||
|
"name": "G Internal",
|
||||||
|
"order": 32,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 128,
|
||||||
|
"name": "G Scene",
|
||||||
|
"order": 128,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 256,
|
||||||
|
"name": "G Freeleech75",
|
||||||
|
"order": 256,
|
||||||
|
"dividerAfter": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 512,
|
||||||
|
"name": "G Freeleech25",
|
||||||
|
"order": 512,
|
||||||
|
"dividerAfter": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"privacy": "normal",
|
||||||
|
"isFloat": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "jennaortegaUHD",
|
||||||
|
"includeCustomFormatWhenRenaming": false,
|
||||||
|
"specifications": [
|
||||||
|
{
|
||||||
|
"name": "jennaortegaUHD",
|
||||||
|
"implementation": "ReleaseGroupSpecification",
|
||||||
|
"implementationName": "Release Group",
|
||||||
|
"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": "(?<=^|[\\s.-])jennaortegaUHD\\b",
|
||||||
|
"type": "textbox",
|
||||||
|
"advanced": false,
|
||||||
|
"privacy": "normal",
|
||||||
|
"isFloat": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -6937,6 +6937,68 @@
|
|||||||
"isFloat": false
|
"isFloat": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WEB",
|
||||||
|
"implementation": "SourceSpecification",
|
||||||
|
"implementationName": "Source",
|
||||||
|
"infoLink": "https://wiki.servarr.com/sonarr/settings#custom-formats-2",
|
||||||
|
"negate": true,
|
||||||
|
"required": false,
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"order": 0,
|
||||||
|
"name": "value",
|
||||||
|
"label": "Source",
|
||||||
|
"value": 3,
|
||||||
|
"type": "select",
|
||||||
|
"advanced": false,
|
||||||
|
"selectOptions": [
|
||||||
|
{
|
||||||
|
"value": 0,
|
||||||
|
"name": "Unknown",
|
||||||
|
"order": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 1,
|
||||||
|
"name": "Television",
|
||||||
|
"order": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 2,
|
||||||
|
"name": "TelevisionRaw",
|
||||||
|
"order": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 3,
|
||||||
|
"name": "Web",
|
||||||
|
"order": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 4,
|
||||||
|
"name": "WebRip",
|
||||||
|
"order": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 5,
|
||||||
|
"name": "DVD",
|
||||||
|
"order": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 6,
|
||||||
|
"name": "Bluray",
|
||||||
|
"order": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 7,
|
||||||
|
"name": "BlurayRaw",
|
||||||
|
"order": 7
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"privacy": "normal",
|
||||||
|
"isFloat": false
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -7281,7 +7343,7 @@
|
|||||||
"name": "value",
|
"name": "value",
|
||||||
"label": "Language",
|
"label": "Language",
|
||||||
"helpText": "Custom Format RegEx is Case Insensitive",
|
"helpText": "Custom Format RegEx is Case Insensitive",
|
||||||
"value": "\\b(FraMeSToR|HQMUX|SiCFoI|playBD|RYU|ElNeekster|CiNEPHiLES|3L|EDV|Kenobi|TRiToN|HDH|NTb|Flights)\\b",
|
"value": "\\b(FraMeSToR|HQMUX|SiCFoI|playBD|RYU|ElNeekster|CiNEPHiLES|3L|EDV|Kenobi|TRiToN|HDH|Flights)\\b",
|
||||||
"type": "textbox",
|
"type": "textbox",
|
||||||
"advanced": false,
|
"advanced": false,
|
||||||
"privacy": "normal",
|
"privacy": "normal",
|
||||||
@@ -11139,5 +11201,32 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "HR",
|
||||||
|
"includeCustomFormatWhenRenaming": true,
|
||||||
|
"specifications": [
|
||||||
|
{
|
||||||
|
"name": "HR",
|
||||||
|
"implementation": "ReleaseGroupSpecification",
|
||||||
|
"implementationName": "Release Group",
|
||||||
|
"infoLink": "https://wiki.servarr.com/sonarr/settings#custom-formats-2",
|
||||||
|
"negate": false,
|
||||||
|
"required": true,
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"order": 0,
|
||||||
|
"name": "value",
|
||||||
|
"label": "Language",
|
||||||
|
"helpText": "Custom Format RegEx is Case Insensitive",
|
||||||
|
"value": "(?<=^|[\\s.-])HR\\b",
|
||||||
|
"type": "textbox",
|
||||||
|
"advanced": false,
|
||||||
|
"privacy": "normal",
|
||||||
|
"isFloat": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -366,6 +366,46 @@
|
|||||||
"minFormatScore": 0,
|
"minFormatScore": 0,
|
||||||
"cutoffFormatScore": 0,
|
"cutoffFormatScore": 0,
|
||||||
"formatItems": [
|
"formatItems": [
|
||||||
|
{
|
||||||
|
"format": 344,
|
||||||
|
"name": "jennaortegaUHD",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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,
|
"format": 335,
|
||||||
"name": "Blu-Ray (Remux)",
|
"name": "Blu-Ray (Remux)",
|
||||||
@@ -366,6 +366,46 @@
|
|||||||
"minFormatScore": 0,
|
"minFormatScore": 0,
|
||||||
"cutoffFormatScore": 500,
|
"cutoffFormatScore": 500,
|
||||||
"formatItems": [
|
"formatItems": [
|
||||||
|
{
|
||||||
|
"format": 344,
|
||||||
|
"name": "jennaortegaUHD",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": -9999
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": 336,
|
||||||
|
"name": "MAX",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"format": 335,
|
"format": 335,
|
||||||
"name": "Blu-Ray (Remux)",
|
"name": "Blu-Ray (Remux)",
|
||||||
@@ -359,6 +359,46 @@
|
|||||||
"minFormatScore": 0,
|
"minFormatScore": 0,
|
||||||
"cutoffFormatScore": 0,
|
"cutoffFormatScore": 0,
|
||||||
"formatItems": [
|
"formatItems": [
|
||||||
|
{
|
||||||
|
"format": 344,
|
||||||
|
"name": "jennaortegaUHD",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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,
|
"format": 335,
|
||||||
"name": "Blu-Ray (Remux)",
|
"name": "Blu-Ray (Remux)",
|
||||||
@@ -359,6 +359,46 @@
|
|||||||
"minFormatScore": 0,
|
"minFormatScore": 0,
|
||||||
"cutoffFormatScore": 1000,
|
"cutoffFormatScore": 1000,
|
||||||
"formatItems": [
|
"formatItems": [
|
||||||
|
{
|
||||||
|
"format": 344,
|
||||||
|
"name": "jennaortegaUHD",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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,
|
"format": 335,
|
||||||
"name": "Blu-Ray (Remux)",
|
"name": "Blu-Ray (Remux)",
|
||||||
@@ -366,6 +366,46 @@
|
|||||||
"minFormatScore": 0,
|
"minFormatScore": 0,
|
||||||
"cutoffFormatScore": 140,
|
"cutoffFormatScore": 140,
|
||||||
"formatItems": [
|
"formatItems": [
|
||||||
|
{
|
||||||
|
"format": 344,
|
||||||
|
"name": "jennaortegaUHD",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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,
|
"format": 335,
|
||||||
"name": "Blu-Ray (Remux)",
|
"name": "Blu-Ray (Remux)",
|
||||||
@@ -366,6 +366,46 @@
|
|||||||
"minFormatScore": 0,
|
"minFormatScore": 0,
|
||||||
"cutoffFormatScore": 0,
|
"cutoffFormatScore": 0,
|
||||||
"formatItems": [
|
"formatItems": [
|
||||||
|
{
|
||||||
|
"format": 344,
|
||||||
|
"name": "jennaortegaUHD",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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,
|
"format": 335,
|
||||||
"name": "Blu-Ray (Remux)",
|
"name": "Blu-Ray (Remux)",
|
||||||
@@ -366,6 +366,46 @@
|
|||||||
"minFormatScore": 0,
|
"minFormatScore": 0,
|
||||||
"cutoffFormatScore": 500,
|
"cutoffFormatScore": 500,
|
||||||
"formatItems": [
|
"formatItems": [
|
||||||
|
{
|
||||||
|
"format": 344,
|
||||||
|
"name": "jennaortegaUHD",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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,
|
"format": 335,
|
||||||
"name": "Blu-Ray (Remux)",
|
"name": "Blu-Ray (Remux)",
|
||||||
@@ -366,6 +366,46 @@
|
|||||||
"minFormatScore": 0,
|
"minFormatScore": 0,
|
||||||
"cutoffFormatScore": 0,
|
"cutoffFormatScore": 0,
|
||||||
"formatItems": [
|
"formatItems": [
|
||||||
|
{
|
||||||
|
"format": 344,
|
||||||
|
"name": "jennaortegaUHD",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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,
|
"format": 335,
|
||||||
"name": "Blu-Ray (Remux)",
|
"name": "Blu-Ray (Remux)",
|
||||||
@@ -366,6 +366,46 @@
|
|||||||
"minFormatScore": 0,
|
"minFormatScore": 0,
|
||||||
"cutoffFormatScore": 1000,
|
"cutoffFormatScore": 1000,
|
||||||
"formatItems": [
|
"formatItems": [
|
||||||
|
{
|
||||||
|
"format": 344,
|
||||||
|
"name": "jennaortegaUHD",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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,
|
"format": 335,
|
||||||
"name": "Blu-Ray (Remux)",
|
"name": "Blu-Ray (Remux)",
|
||||||
@@ -359,10 +359,50 @@
|
|||||||
"minFormatScore": 0,
|
"minFormatScore": 0,
|
||||||
"cutoffFormatScore": 0,
|
"cutoffFormatScore": 0,
|
||||||
"formatItems": [
|
"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,
|
"format": 335,
|
||||||
"name": "Blu-Ray (Remux)",
|
"name": "Blu-Ray (Remux)",
|
||||||
"score": 0
|
"score": 60
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"format": 334,
|
"format": 334,
|
||||||
@@ -359,10 +359,50 @@
|
|||||||
"minFormatScore": 0,
|
"minFormatScore": 0,
|
||||||
"cutoffFormatScore": 320,
|
"cutoffFormatScore": 320,
|
||||||
"formatItems": [
|
"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,
|
"format": 335,
|
||||||
"name": "Blu-Ray (Remux)",
|
"name": "Blu-Ray (Remux)",
|
||||||
"score": 0
|
"score": 60
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"format": 334,
|
"format": 334,
|
||||||
@@ -232,6 +232,21 @@
|
|||||||
"minFormatScore": 0,
|
"minFormatScore": 0,
|
||||||
"cutoffFormatScore": 0,
|
"cutoffFormatScore": 0,
|
||||||
"formatItems": [
|
"formatItems": [
|
||||||
|
{
|
||||||
|
"format": 229,
|
||||||
|
"name": "HR",
|
||||||
|
"score": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": 228,
|
||||||
|
"name": "MAX",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": 227,
|
||||||
|
"name": "h265 (4k)",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"format": 226,
|
"format": 226,
|
||||||
"name": "PCM",
|
"name": "PCM",
|
||||||
@@ -232,6 +232,21 @@
|
|||||||
"minFormatScore": 0,
|
"minFormatScore": 0,
|
||||||
"cutoffFormatScore": 500,
|
"cutoffFormatScore": 500,
|
||||||
"formatItems": [
|
"formatItems": [
|
||||||
|
{
|
||||||
|
"format": 229,
|
||||||
|
"name": "HR",
|
||||||
|
"score": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": 228,
|
||||||
|
"name": "MAX",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": 227,
|
||||||
|
"name": "h265 (4k)",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"format": 226,
|
"format": 226,
|
||||||
"name": "PCM",
|
"name": "PCM",
|
||||||
@@ -218,6 +218,21 @@
|
|||||||
"minFormatScore": 0,
|
"minFormatScore": 0,
|
||||||
"cutoffFormatScore": 0,
|
"cutoffFormatScore": 0,
|
||||||
"formatItems": [
|
"formatItems": [
|
||||||
|
{
|
||||||
|
"format": 229,
|
||||||
|
"name": "HR",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": 228,
|
||||||
|
"name": "MAX",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": 227,
|
||||||
|
"name": "h265 (4k)",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"format": 226,
|
"format": 226,
|
||||||
"name": "PCM",
|
"name": "PCM",
|
||||||
@@ -218,6 +218,21 @@
|
|||||||
"minFormatScore": 0,
|
"minFormatScore": 0,
|
||||||
"cutoffFormatScore": 1000,
|
"cutoffFormatScore": 1000,
|
||||||
"formatItems": [
|
"formatItems": [
|
||||||
|
{
|
||||||
|
"format": 229,
|
||||||
|
"name": "HR",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": 228,
|
||||||
|
"name": "MAX",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": 227,
|
||||||
|
"name": "h265 (4k)",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"format": 226,
|
"format": 226,
|
||||||
"name": "PCM",
|
"name": "PCM",
|
||||||
@@ -239,6 +239,21 @@
|
|||||||
"minFormatScore": 0,
|
"minFormatScore": 0,
|
||||||
"cutoffFormatScore": 140,
|
"cutoffFormatScore": 140,
|
||||||
"formatItems": [
|
"formatItems": [
|
||||||
|
{
|
||||||
|
"format": 229,
|
||||||
|
"name": "HR",
|
||||||
|
"score": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": 228,
|
||||||
|
"name": "MAX",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": 227,
|
||||||
|
"name": "h265 (4k)",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"format": 226,
|
"format": 226,
|
||||||
"name": "PCM",
|
"name": "PCM",
|
||||||
@@ -239,6 +239,21 @@
|
|||||||
"minFormatScore": 0,
|
"minFormatScore": 0,
|
||||||
"cutoffFormatScore": 0,
|
"cutoffFormatScore": 0,
|
||||||
"formatItems": [
|
"formatItems": [
|
||||||
|
{
|
||||||
|
"format": 229,
|
||||||
|
"name": "HR",
|
||||||
|
"score": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": 228,
|
||||||
|
"name": "MAX",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": 227,
|
||||||
|
"name": "h265 (4k)",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"format": 226,
|
"format": 226,
|
||||||
"name": "PCM",
|
"name": "PCM",
|
||||||
@@ -239,6 +239,21 @@
|
|||||||
"minFormatScore": 0,
|
"minFormatScore": 0,
|
||||||
"cutoffFormatScore": 500,
|
"cutoffFormatScore": 500,
|
||||||
"formatItems": [
|
"formatItems": [
|
||||||
|
{
|
||||||
|
"format": 229,
|
||||||
|
"name": "HR",
|
||||||
|
"score": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": 228,
|
||||||
|
"name": "MAX",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": 227,
|
||||||
|
"name": "h265 (4k)",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"format": 226,
|
"format": 226,
|
||||||
"name": "PCM",
|
"name": "PCM",
|
||||||
@@ -232,6 +232,21 @@
|
|||||||
"minFormatScore": 0,
|
"minFormatScore": 0,
|
||||||
"cutoffFormatScore": 500,
|
"cutoffFormatScore": 500,
|
||||||
"formatItems": [
|
"formatItems": [
|
||||||
|
{
|
||||||
|
"format": 229,
|
||||||
|
"name": "HR",
|
||||||
|
"score": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": 228,
|
||||||
|
"name": "MAX",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": 227,
|
||||||
|
"name": "h265 (4k)",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"format": 226,
|
"format": 226,
|
||||||
"name": "PCM",
|
"name": "PCM",
|
||||||
@@ -232,6 +232,21 @@
|
|||||||
"minFormatScore": 0,
|
"minFormatScore": 0,
|
||||||
"cutoffFormatScore": 0,
|
"cutoffFormatScore": 0,
|
||||||
"formatItems": [
|
"formatItems": [
|
||||||
|
{
|
||||||
|
"format": 229,
|
||||||
|
"name": "HR",
|
||||||
|
"score": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": 228,
|
||||||
|
"name": "MAX",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": 227,
|
||||||
|
"name": "h265 (4k)",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"format": 226,
|
"format": 226,
|
||||||
"name": "PCM",
|
"name": "PCM",
|
||||||
@@ -239,6 +239,21 @@
|
|||||||
"minFormatScore": 0,
|
"minFormatScore": 0,
|
||||||
"cutoffFormatScore": 0,
|
"cutoffFormatScore": 0,
|
||||||
"formatItems": [
|
"formatItems": [
|
||||||
|
{
|
||||||
|
"format": 229,
|
||||||
|
"name": "HR",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": 228,
|
||||||
|
"name": "MAX",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": 227,
|
||||||
|
"name": "h265 (4k)",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"format": 226,
|
"format": 226,
|
||||||
"name": "PCM",
|
"name": "PCM",
|
||||||
@@ -247,7 +262,7 @@
|
|||||||
{
|
{
|
||||||
"format": 225,
|
"format": 225,
|
||||||
"name": "Blu-Ray (Remux)",
|
"name": "Blu-Ray (Remux)",
|
||||||
"score": 0
|
"score": 60
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"format": 224,
|
"format": 224,
|
||||||
@@ -239,6 +239,21 @@
|
|||||||
"minFormatScore": 0,
|
"minFormatScore": 0,
|
||||||
"cutoffFormatScore": 320,
|
"cutoffFormatScore": 320,
|
||||||
"formatItems": [
|
"formatItems": [
|
||||||
|
{
|
||||||
|
"format": 229,
|
||||||
|
"name": "HR",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": 228,
|
||||||
|
"name": "MAX",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": 227,
|
||||||
|
"name": "h265 (4k)",
|
||||||
|
"score": 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"format": 226,
|
"format": 226,
|
||||||
"name": "PCM",
|
"name": "PCM",
|
||||||
@@ -247,7 +262,7 @@
|
|||||||
{
|
{
|
||||||
"format": 225,
|
"format": 225,
|
||||||
"name": "Blu-Ray (Remux)",
|
"name": "Blu-Ray (Remux)",
|
||||||
"score": 0
|
"score": 60
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"format": 224,
|
"format": 224,
|
||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
30
setup.py
30
setup.py
@@ -1,24 +1,24 @@
|
|||||||
config_content = """
|
config_content = """
|
||||||
instances:
|
instances:
|
||||||
master:
|
radarr:
|
||||||
sonarr:
|
- name: "Master"
|
||||||
base_url: "http://localhost:8989"
|
|
||||||
api_key: "API_KEY"
|
|
||||||
radarr:
|
|
||||||
base_url: "http://localhost:7878"
|
base_url: "http://localhost:7878"
|
||||||
api_key: "API_KEY"
|
api_key: "API_KEY"
|
||||||
extras:
|
- name: "4k-radarr"
|
||||||
sonarr:
|
base_url: "http://localhost:7887"
|
||||||
- name: "4k-sonarr"
|
api_key: "API_KEY"
|
||||||
base_url: "http://localhost:8998"
|
sonarr:
|
||||||
api_key: "API_KEY"
|
- name: "Master"
|
||||||
radarr:
|
base_url: "http://localhost:8989"
|
||||||
- name: "4k-radarr"
|
api_key: "API_KEY"
|
||||||
base_url: "http://localhost:7887"
|
- name: "4k-sonarr"
|
||||||
api_key: "API_KEY"
|
base_url: "http://localhost:8998"
|
||||||
|
api_key: "API_KEY"
|
||||||
settings:
|
settings:
|
||||||
export_path: "./exports"
|
export_path: "./exports"
|
||||||
|
import_path: "./imports"
|
||||||
|
ansi_colors: "true"
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with open('config.yml', 'w') as file:
|
with open('config.yml', 'w') as file:
|
||||||
|
|||||||
57
syncarr.py
57
syncarr.py
@@ -1,47 +1,22 @@
|
|||||||
import yaml
|
from exportarr import export_custom_formats, export_quality_profiles
|
||||||
import json
|
from importarr import import_custom_formats, import_quality_profiles
|
||||||
import shutil
|
from helpers import load_config, get_app_choice
|
||||||
import os
|
|
||||||
import exportarr # Assuming this module contains the export functions
|
|
||||||
import importarr # Assuming this module contains the import functions
|
|
||||||
|
|
||||||
def sync_data(sync_mode=False):
|
def main():
|
||||||
# Load configuration from YAML file
|
app = get_app_choice().lower() # Convert to lowercase
|
||||||
with open('config.yml', 'r') as config_file:
|
config = load_config() # Load the entire configuration
|
||||||
config = yaml.safe_load(config_file)
|
|
||||||
|
|
||||||
# Specify the temporary path where files will be saved
|
# Now app will be 'radarr' or 'sonarr', matching the keys in the config dictionary
|
||||||
temp_cf_path = './temp_directory/custom_formats'
|
master_instance = next((inst for inst in config['instances'][app] if inst['name'] == 'Master'), None)
|
||||||
temp_qf_path = './temp_directory/quality_profiles'
|
extra_instances = [inst for inst in config['instances'][app] if inst['name'] != 'Master']
|
||||||
|
|
||||||
# Get user choice for app (radarr/sonarr)
|
if master_instance:
|
||||||
app_choice = input("Select the app you want to sync:\n1. Radarr\n2. Sonarr\nEnter your choice (1 or 2): ").strip()
|
export_custom_formats(app, [master_instance], config)
|
||||||
|
export_quality_profiles(app, [master_instance], config)
|
||||||
|
|
||||||
while app_choice not in ["1", "2"]:
|
if extra_instances:
|
||||||
print("Invalid input. Please enter 1 for Radarr or 2 for Sonarr.")
|
import_custom_formats(app, extra_instances)
|
||||||
app_choice = input("Enter your choice (1 or 2): ").strip()
|
import_quality_profiles(app, extra_instances)
|
||||||
|
|
||||||
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 __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Set sync_mode to True to enable automatic selection of all files during import
|
main()
|
||||||
sync_data(sync_mode=True)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user