4 Commits

Author SHA1 Message Date
santiagosayshey
2e5cabe7ab v0.2.4
## Features
- Implemented #12 
- Enhanced message formatting and user prompts for deletions.

## Additions
- ETHEL SCENE release group added.
- FLiGHTS group to missing HDR10.
- Overhauled h265 custom formats and added new h265 4k format.
- MAX WEB source added to custom formats.

## Improvements
- Script to generate initial config file.
- Removed tracking of config.yml for security.
- Updated README for install process and new delete feature.
2024-02-03 11:32:04 +10:30
santiagosayshey
2c764f993c updated readme 2024-01-27 15:16:54 +10:30
santiagosayshey
fc8196dc10 v0.2.3
- changed from json to yaml for config
- added export_path setting to set where exported files go : fixes https://github.com/santiagosayshey/Profilarr/issues/5
- exports now save to {export_path}/{app_type}/{app_name}
- can choose which instance to manually import to
- sync automatically chooses correct custom formats / quality profiles for the app type
- updated readme with usage examples
- added install requirements

Closes #4, #5
2024-01-27 15:14:04 +10:30
santiagosayshey
c39d477deb Update README.md
temp fix for issue #4
2024-01-27 09:10:36 +10:30
12 changed files with 1760 additions and 203 deletions

4
.gitignore vendored
View File

@@ -158,3 +158,7 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
config.yml
exports/

300
README.md
View File

@@ -17,27 +17,22 @@ Profilarr is a Python-based tool designed to add import/export/sync functionalit
### 📦 Dependencies
- `requests` (Install using `pip install requests`)
- run `pip install -r requirements.txt` to install dependencies.
### Initial Setup
1. Download the latest Profilarr package from the release section.
2. Extract its contents into a folder.
3. Open the `config.json` file in a text editor.
- Add your Radarr / Sonarr API key and modify the base URL as necessary.
- If importing / exporting, only change the master installation's API key and base URL.
- If syncing, add the API keys and base URLs of all instances you want to sync.
- The master install will be the one that all other instances sync to.
4. Save the changes.
3. Run `python setup.py` in your command line interface to generate a config file.
- This will create a `config.yml` file in the same directory as `setup.py`.
4. Open the `config.yml` file in a text editor.
- Add the URL and API key to the master instances of Radarr / Sonarr.
- If syncing, add the URL, API key and a name to each extra instance of Radarr / Sonarr.
- If exporting, adjust the `export_path` to your desired export location.
5. Save the changes.
## 🚀 Usage
### Exporting
1. Run `python exportarr.py` in your command line interface.
2. Follow the on-screen prompts to select the app (Radarr or Sonarr) and the data (Custom Formats or Quality Profiles) you want to export.
3. Exported data will be saved in respective directories within the tool's folder.
### Importing
1. Run `python importarr.py` in your command line interface.
@@ -45,16 +40,285 @@ Profilarr is a Python-based tool designed to add import/export/sync functionalit
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
```bash
PS Z:\Profilarr> py importarr.py
Available instances to import to:
1. Sonarr [Master]
2. Radarr [Master]
3. Sonarr [4k-sonarr]
4. Radarr [4k-radarr]
Enter the number of the instance to import to: 4
Choose what to import:
1. Custom Formats
2. Quality Profiles
Enter your choice (1/2): 1
Available files:
1. Custom Formats (Radarr).json
Select a file to import (or 'all' for all files): 1
Adding custom format 'D-Z0N3': SUCCESS
Adding custom format 'DON': SUCCESS
Adding custom format 'EbP': SUCCESS
Adding custom format 'Geek': SUCCESS
Adding custom format 'TayTo': SUCCESS
Adding custom format 'ZQ': SUCCESS
Adding custom format 'VietHD': SUCCESS
Adding custom format 'CtrlHD': SUCCESS
Adding custom format 'HiFi': SUCCESS
Adding custom format 'FoRM': SUCCESS
Adding custom format 'HiDt': SUCCESS
Adding custom format 'SA89': SUCCESS
...
Successfully added 0 custom formats, updated 131 custom formats.
```
#### Quality Profile Import Example
```bash
PS Z:\Profilarr> py importarr.py
Available instances to import to:
1. Sonarr [Master]
2. Radarr [Master]
3. Sonarr [4k-sonarr]
4. Radarr [4k-radarr]
Enter the number of the instance to import to: 4
Choose what to import:
1. Custom Formats
2. Quality Profiles
Enter your choice (1/2): 2
Available files:
1. 1080p Balanced (Radarr).json
2. 1080p Balanced (Single Grab) (Radarr).json
3. 1080p h265 Balanced (Radarr).json
4. 1080p h265 Balanced (Single Grab) (Radarr).json
5. 1080p Optimal (Radarr).json
6. 1080p Optimal (Single Grab) (Radarr).json
7. 1080p Transparent (Double Grab) (Radarr).json
8. 1080p Transparent (Radarr).json
9. 1080p Transparent (Single Grab) (Radarr).json
10. 2160p Optimal (Radarr).json
11. 2160p Optimal (Single Grab) (Radarr).json
Select a file to import (or 'all' for all files): all
Successfully added Quality Profile 1080p Balanced
Successfully added Quality Profile 1080p Balanced (Single Grab)
Successfully added Quality Profile 1080p h265 Balanced
Successfully added Quality Profile 1080p h265 Balanced (Single Grab)
Successfully added Quality Profile 1080p Optimal
Successfully added Quality Profile 1080p Optimal (Single Grab)
Successfully added Quality Profile 1080p Transparent (Double Grab)
Successfully added Quality Profile 1080p Transparent
Successfully added Quality Profile 1080p Transparent (Single Grab)
Successfully added Quality Profile 2160p Optimal
Successfully added Quality Profile 2160p Optimal (Single Grab)
PS Z:\Profilarr>
```
### Exporting
1. Run `python exportarr.py` in your command line interface.
2. Choose the instance you want to export from.
3. Choose the data you want to export.
4. The data will be exported to `exports/{instance_type}/{instance_name}/{data_type}`.
#### Example
```bash
PS Z:\Profilarr> py exportarr.py
Available sources to export from:
1. Sonarr [Master]
2. Radarr [Master]
3. Sonarr [4k-sonarr]
4. Radarr [4k-radarr]
Enter the number of the app to export from: 2
Choose what to export:
1. Custom Formats
2. Quality Profiles
3. Both
Enter your choice (1/2/3): 3
Attempting to access Radarr at http://localhost:7878
Found 131 custom formats.
- D-Z0N3
- DON
- EbP
- Geek
- TayTo
- ZQ
- VietHD
- CtrlHD
- HiFi
- FoRM
... and 121 more.
Saved to './exports\radarr\master\custom_formats\Custom Formats (Radarr).json'
Attempting to access Radarr at http://localhost:7878
Found 13 quality profiles.
- 1080p Optimal
- 2160p Optimal
- 1080p Balanced
- 1080p Transparent
- 1080p Transparent (Double Grab)
- 1080p Transparent (Single Grab)
- 1080p Balanced (Single Grab)
- 1080p h265 Balanced
- 1080p h265 Balanced (Single Grab)
- 1080p x265 HDR Transparent
... and 3 more.
Saved to 'exports\radarr\master\profiles'
PS Z:\Profilarr>
```
### Syncing
1. Run `python syncarr.py` in your command line interface.
2. The script will automatically export data from the master instance and import it to all other instances specified in `config.json`.
3. This feature is designed to manage multiple Radarr/Sonarr instances, syncing profiles and formats seamlessly.
#### Example
```bash
PS Z:\Profilarr> py syncarr.py
Select the app you want to sync:
1. Radarr
2. Sonarr
Enter your choice (1 or 2): 2
Attempting to access Sonarr at http://localhost:8989
Found 135 custom formats.
- D-Z0N3
- DON
- EbP
- Geek
- TayTo
- ZQ
- VietHD
- CtrlHD
- HiFi
- FoRM
... and 125 more.
Saved to './temp_directory/custom_formats\Custom Formats (Sonarr).json'
Attempting to access Sonarr at http://localhost:8989
Found 11 quality profiles.
- 1080p Transparent
- 2160p Optimal
- 1080p Transparent (Single Grab)
- 1080p Transparent (Double Grab)
- 1080p Balanced
- 1080p Balanced (Single Grab)
- 1080p h265 Balanced
- 1080p h265 Balanced (Single Grab)
- 1080p Optimal
- 1080p Optimal (Single Grab)
... and 1 more.
Saved to 'temp_directory\quality_profiles'
Importing to instance: 4k-sonarr
Adding custom format 'D-Z0N3': SUCCESS
Adding custom format 'DON': SUCCESS
Adding custom format 'EbP': SUCCESS
Adding custom format 'Geek': SUCCESS
Adding custom format 'TayTo': SUCCESS
Adding custom format 'ZQ': SUCCESS
Adding custom format 'VietHD': SUCCESS
Adding custom format 'CtrlHD': SUCCESS
Adding custom format 'HiFi': SUCCESS
... and 125 more.
Successfully added 135 custom formats, updated 0 custom formats.
Successfully added Quality Profile 1080p Balanced (Single Grab)
Successfully added Quality Profile 1080p Balanced
Successfully added Quality Profile 1080p h265 Balanced
Successfully added Quality Profile 1080p h265 Balanced (Single Grab)
Successfully added Quality Profile 1080p Optimal (Single Grab)
Successfully added Quality Profile 1080p Optimal
Successfully added Quality Profile 1080p Transparent (Double Grab)
Successfully added Quality Profile 1080p Transparent (Single Grab)
Successfully added Quality Profile 1080p Transparent
Successfully added Quality Profile 2160p Optimal (Single Grab)
Successfully added Quality Profile 2160p Optimal
Deleted temporary directory: ./temp_directory
PS Z:\Profilarr>
```
### Deleting
1. Run `python deletarr.py` in your command line interface.
2. Select the instance from which you wish to delete data.
3. Choose between deleting Custom Formats or Quality Profiles.
4. Select specific items by typing their numbers separated by commas, or type 'all' to delete everything.
#### Example: Deleting Custom Formats
```plaintext
PS Z:\Profilarr> python deletarr.py
Available instances to delete from:
1. Sonarr [Master]
2. Radarr [Master]
Enter the number of the instance to delete from: 2
Choose what to delete:
1. Custom Formats
2. Quality Profiles
Enter your choice (1/2): 1
Deleting selected custom formats...
Available items:
1. UHDBits
2. Dolby Vision w/out Fallback
...
132. h265 (4k)
133. MAX
Your choice: all
Deleting custom format 'UHDBits': SUCCESS
...
Deleting custom format 'MAX': SUCCESS
```
#### Example: Deleting Quality Profiles
```plaintext
PS Z:\Profilarr> python deletarr.py
Choose what to delete:
1. Custom Formats
2. Quality Profiles
Enter your choice (1/2): 2
Deleting selected quality profiles...
Available items:
1. 1080p Balanced
...
11. 2160p Optimal
Your choice: all
Deleting quality profile '1080p Balanced': SUCCESS
...
Deleting quality profile '2160p Optimal': SUCCESS
```
### Radarr and Sonarr Compatibility
- Custom formats _can_ be imported and exported between Radarr and Sonarr (but might not work as expected).
- Quality profiles are not directly interchangeable between Radarr and Sonarr due to differences in quality source names. If you want to use the same profile in both apps, you will need to manually edit the profile's quality source names before importing it.
- You are only able to import / sync files to the app that is included in the file name (e.g. `Radarr` or `Sonarr`).
- It is possible to manually rename the files to import them to the other app, but this is not recommended.
- Custom Formats will succesfully import, but will require manual editing to work with the other app, i.e. you must adjust the quality sources to match the other app's naming scheme.
- Quality Profiles will not import at all, as they are not compatible with the other app. It is possible to import them manually by editing the json directly, but this is not recommended.
- In future, I may add a feature to automatically convert profiles between the two apps, but this is not currently a priority.
## 🌟 Upcoming Features
@@ -62,7 +326,11 @@ Profilarr is a Python-based tool designed to add import/export/sync functionalit
- **User Interface (UI):** Development of a graphical user interface (GUI) for easier and more intuitive interaction with Profilarr. This UI will cater to users who prefer graphical over command-line interactions.
- **Automatic Updates:** Implement an auto-update mechanism for Profilarr, ensuring users always have access to the latest features, improvements, and bug fixes without manual intervention.
# TRASH Guides
## Contributing
- I've added a docker compose file for testing custom formats / quality profiles. Run `docker-compose up -d` to start the Radarr/ Sonarr test containers. Add your API keys to the `config.yml` file and begin testing!
# TRaSH Guides
Some custom formats found here have been interated on from the trash guides. Credit for these goes entirely to trash, and can be found on their site here. It is not my intention to steal their work, but rather to build on it and make it more accessible to the average user through my quality profiles. Please check out their site for more information on their work.

View File

@@ -1,38 +0,0 @@
{
"master": {
"sonarr": {
"base_url": "http://localhost:8989",
"api_key": "API_KEY_HERE"
},
"radarr": {
"base_url": "http://localhost:7878",
"api_key": "API_KEY_HERE"
}
},
"extra_installations": {
"radarr": [
{
"name": "extra_radarr1",
"base_url": "http://localhost:7788",
"api_key": "API_KEY_HERE"
},
{
"name": "extra_radarr2",
"base_url": "http://localhost:7789",
"api_key": "API_KEY_HERE"
}
],
"sonarr": [
{
"name": "extra_sonarr1",
"base_url": "http://localhost:8988",
"api_key": "API_KEY_HERE"
},
{
"name": "extra_sonarr2",
"base_url": "http://localhost:8987",
"api_key": "API_KEY_HERE"
}
]
}
}

View File

@@ -8928,6 +8928,27 @@
"isFloat": false
}
]
},
{
"name": "ETHEL",
"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.-])ETHEL\\b",
"type": "textbox",
"advanced": false,
"privacy": "normal",
"isFloat": false
}
]
}
]
},
@@ -9272,7 +9293,7 @@
"name": "value",
"label": "Regular Expression",
"helpText": "Custom Format RegEx is Case Insensitive",
"value": "\\b(FraMeSToR|HQMUX|SiCFoI|playBD|RYU|ElNeekster|CiNEPHiLES|3L|EDV|Kenobi|TRiToN|HDH|NTb)\\b",
"value": "\\b(FraMeSToR|HQMUX|SiCFoI|playBD|RYU|ElNeekster|CiNEPHiLES|3L|EDV|Kenobi|TRiToN|HDH|NTb|Flights)\\b",
"type": "textbox",
"advanced": false,
"privacy": "normal",
@@ -11896,7 +11917,7 @@
"name": "value",
"label": "Regular Expression",
"helpText": "Custom Format RegEx is Case Insensitive",
"value": "^(?!.*(?i:remux)).*([hH]\\s*\\.?\\s*265)",
"value": "(?i)h\\s*\\.?\\s*265",
"type": "textbox",
"advanced": false,
"privacy": "normal",
@@ -11938,7 +11959,7 @@
"name": "value",
"label": "Regular Expression",
"helpText": "Custom Format RegEx is Case Insensitive",
"value": "(?i)(REMUX|DVDRip)",
"value": "Remux",
"type": "textbox",
"advanced": false,
"privacy": "normal",
@@ -11948,8 +11969,8 @@
},
{
"name": "WEB",
"implementation": "ReleaseTitleSpecification",
"implementationName": "Release Title",
"implementation": "SourceSpecification",
"implementationName": "Source",
"infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2",
"negate": false,
"required": true,
@@ -11957,11 +11978,142 @@
{
"order": 0,
"name": "value",
"label": "Regular Expression",
"helpText": "Custom Format RegEx is Case Insensitive",
"value": "(?<!dts[ .-]?hd[ .-]?)ma\\b(?=.*\\bweb[ ._-]?(dl|rip)\\b)|\\b(amzn|amazon)\\b|\\b(atvp|aptv|Apple TV\\+)\\b|\\b(dsnp|dsny|disney|Disney\\+)\\b|\\b(nf|netflix)\\b|\\b(hmax|hbom|hbo[ ._-]max)\\b(?=[ ._-]web[ ._-]?(dl|rip)\\b)|\\b(hulu)\\b|\\b(pcok|peacock)\\b|\\b(pmtp|Paramount Plus)\\b|\\b(it|itunes)\\b(?=[ ._-]web[ ._-]?(dl|rip)\\b)",
"type": "textbox",
"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": "4k",
"implementation": "ResolutionSpecification",
"implementationName": "Resolution",
"infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2",
"negate": true,
"required": true,
"fields": [
{
"order": 0,
"name": "value",
"label": "Resolution",
"value": 2160,
"type": "select",
"advanced": false,
"selectOptions": [
{
"value": 0,
"name": "Unknown",
"order": 0,
"dividerAfter": false
},
{
"value": 360,
"name": "R360p",
"order": 360,
"dividerAfter": false
},
{
"value": 480,
"name": "R480p",
"order": 480,
"dividerAfter": false
},
{
"value": 540,
"name": "R540p",
"order": 540,
"dividerAfter": false
},
{
"value": 576,
"name": "R576p",
"order": 576,
"dividerAfter": false
},
{
"value": 720,
"name": "R720p",
"order": 720,
"dividerAfter": false
},
{
"value": 1080,
"name": "R1080p",
"order": 1080,
"dividerAfter": false
},
{
"value": 2160,
"name": "R2160p",
"order": 2160,
"dividerAfter": false
}
],
"privacy": "normal",
"isFloat": false
}
@@ -12230,5 +12382,417 @@
]
}
]
},
{
"name": "MAX",
"includeCustomFormatWhenRenaming": true,
"specifications": [
{
"name": "Max",
"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": "\\b((?<!hbo[ ._-])max)\\b(?=[ ._-]web[ ._-]?(dl|rip)\\b)",
"type": "textbox",
"advanced": false,
"privacy": "normal",
"isFloat": false
}
]
},
{
"name": "WEBDL",
"implementation": "SourceSpecification",
"implementationName": "Source",
"infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2",
"negate": false,
"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": "WEBRIP",
"implementation": "SourceSpecification",
"implementationName": "Source",
"infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2",
"negate": false,
"required": false,
"fields": [
{
"order": 0,
"name": "value",
"label": "Source",
"value": 8,
"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": "h265 (4k)",
"includeCustomFormatWhenRenaming": false,
"specifications": [
{
"name": "h265",
"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": "(?i)h\\s*\\.?\\s*265",
"type": "textbox",
"advanced": false,
"privacy": "normal",
"isFloat": false
}
]
},
{
"name": "Disc",
"implementation": "ReleaseTitleSpecification",
"implementationName": "Release Title",
"infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2",
"negate": true,
"required": true,
"fields": [
{
"order": 0,
"name": "value",
"label": "Regular Expression",
"helpText": "Custom Format RegEx is Case Insensitive",
"value": "^(?!.*\\b((?<!HD[._ -]|HD)DVD|BDRip|MKV|XviD|WMV|d3g|BDREMUX|REMUX|^(?=.*1080p)(?=.*HEVC)|[xh][-_. ]?26[45]|German.*DL|((?<=\\d{4}).*German.*(DL)?)(?=.*\\b(AVC|HEVC|VC[-_. ]?1|MVC|MPEG[-_. ]?2)\\b))\\b)(((?=.*\\b(Blu[-_. ]?ray|BD|HD[-_. ]?DVD)\\b)(?=.*\\b(AVC|HEVC|VC[-_. ]?1|MVC|MPEG[-_. ]?2|BDMV|ISO)\\b))|^((?=.*\\b(^((?=.*\\b((.*_)?COMPLETE.*|Dis[ck])\\b)(?=.*(Blu[-_. ]?ray|HD[-_. ]?DVD)))|3D[-_. ]?BD|BR[-_. ]?DISK|Full[-_. ]?Blu[-_. ]?ray|^((?=.*((BD|UHD)[-_. ]?(25|50|66|100|ISO)))))))).*|(?i)(DVD9|DVD5|NTSC|PAL|VOB IFO|VC-1|AVC|MPEG-2|\\bCOMPLETE[-.\\s]?(?:UHD[-.\\s])?BLU[-.\\s]?RAY\\b|\\bCOMPLETE BLURAY\\b|\\bBR-Disk\\b)",
"type": "textbox",
"advanced": false,
"privacy": "normal",
"isFloat": false
}
]
},
{
"name": "Remux",
"implementation": "ReleaseTitleSpecification",
"implementationName": "Release Title",
"infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2",
"negate": true,
"required": true,
"fields": [
{
"order": 0,
"name": "value",
"label": "Regular Expression",
"helpText": "Custom Format RegEx is Case Insensitive",
"value": "Remux",
"type": "textbox",
"advanced": false,
"privacy": "normal",
"isFloat": false
}
]
},
{
"name": "WEB",
"implementation": "SourceSpecification",
"implementationName": "Source",
"infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2",
"negate": false,
"required": true,
"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": "4k",
"implementation": "ResolutionSpecification",
"implementationName": "Resolution",
"infoLink": "https://wiki.servarr.com/radarr/settings#custom-formats-2",
"negate": false,
"required": true,
"fields": [
{
"order": 0,
"name": "value",
"label": "Resolution",
"value": 2160,
"type": "select",
"advanced": false,
"selectOptions": [
{
"value": 0,
"name": "Unknown",
"order": 0,
"dividerAfter": false
},
{
"value": 360,
"name": "R360p",
"order": 360,
"dividerAfter": false
},
{
"value": 480,
"name": "R480p",
"order": 480,
"dividerAfter": false
},
{
"value": 540,
"name": "R540p",
"order": 540,
"dividerAfter": false
},
{
"value": 576,
"name": "R576p",
"order": 576,
"dividerAfter": false
},
{
"value": 720,
"name": "R720p",
"order": 720,
"dividerAfter": false
},
{
"value": 1080,
"name": "R1080p",
"order": 1080,
"dividerAfter": false
},
{
"value": 2160,
"name": "R2160p",
"order": 2160,
"dividerAfter": false
}
],
"privacy": "normal",
"isFloat": false
}
]
}
]
}
]

View File

@@ -6916,6 +6916,27 @@
"isFloat": false
}
]
},
{
"name": "ETHEL",
"implementation": "ReleaseGroupSpecification",
"implementationName": "Release Group",
"infoLink": "https://wiki.servarr.com/sonarr/settings#custom-formats-2",
"negate": false,
"required": false,
"fields": [
{
"order": 0,
"name": "value",
"label": "Language",
"helpText": "Custom Format RegEx is Case Insensitive",
"value": "(?<=^|[\\s.-])ETHEL\\b",
"type": "textbox",
"advanced": false,
"privacy": "normal",
"isFloat": false
}
]
}
]
},
@@ -7260,7 +7281,7 @@
"name": "value",
"label": "Language",
"helpText": "Custom Format RegEx is Case Insensitive",
"value": "\\b(FraMeSToR|HQMUX|SiCFoI|playBD|RYU|ElNeekster|CiNEPHiLES|3L|EDV|Kenobi|TRiToN|HDH|TekMUX|NTb)\\b",
"value": "\\b(FraMeSToR|HQMUX|SiCFoI|playBD|RYU|ElNeekster|CiNEPHiLES|3L|EDV|Kenobi|TRiToN|HDH|NTb|Flights)\\b",
"type": "textbox",
"advanced": false,
"privacy": "normal",
@@ -10378,28 +10399,7 @@
"name": "value",
"label": "Language",
"helpText": "Custom Format RegEx is Case Insensitive",
"value": "^(?!.*(?i:remux)).*([hH]\\\\s*\\\\.?\\\\s*265)",
"type": "textbox",
"advanced": false,
"privacy": "normal",
"isFloat": false
}
]
},
{
"name": "Disc",
"implementation": "ReleaseTitleSpecification",
"implementationName": "Release Title",
"infoLink": "https://wiki.servarr.com/sonarr/settings#custom-formats-2",
"negate": true,
"required": true,
"fields": [
{
"order": 0,
"name": "value",
"label": "Language",
"helpText": "Custom Format RegEx is Case Insensitive",
"value": "^(?!.*\\b((?<!HD[._ -]|HD)DVD|BDRip|MKV|XviD|WMV|d3g|BDREMUX|REMUX|^(?=.*1080p)(?=.*HEVC)|[xh][-_. ]?26[45]|German.*DL|((?<=\\d{4}).*German.*(DL)?)(?=.*\\b(AVC|HEVC|VC[-_. ]?1|MVC|MPEG[-_. ]?2)\\b))\\b)(((?=.*\\b(Blu[-_. ]?ray|BD|HD[-_. ]?DVD)\\b)(?=.*\\b(AVC|HEVC|VC[-_. ]?1|MVC|MPEG[-_. ]?2|BDMV|ISO)\\b))|^((?=.*\\b(^((?=.*\\b((.*_)?COMPLETE.*|Dis[ck])\\b)(?=.*(Blu[-_. ]?ray|HD[-_. ]?DVD)))|3D[-_. ]?BD|BR[-_. ]?DISK|Full[-_. ]?Blu[-_. ]?ray|^((?=.*((BD|UHD)[-_. ]?(25|50|66|100|ISO)))))))).*|(?i)(DVD9|DVD5|NTSC|PAL|VOB IFO|VC-1|AVC|MPEG-2|\\bCOMPLETE[-.\\s]?(?:UHD[-.\\s])?BLU[-.\\s]?RAY\\b|\\bCOMPLETE BLURAY\\b|\\bBR-Disk\\b)",
"value": "(?i)h\\s*\\.?\\s*265",
"type": "textbox",
"advanced": false,
"privacy": "normal",
@@ -10420,13 +10420,137 @@
"name": "value",
"label": "Language",
"helpText": "Custom Format RegEx is Case Insensitive",
"value": "(?i)(REMUX|DVDRip)",
"value": "Remux",
"type": "textbox",
"advanced": false,
"privacy": "normal",
"isFloat": false
}
]
},
{
"name": "WEB",
"implementation": "SourceSpecification",
"implementationName": "Source",
"infoLink": "https://wiki.servarr.com/sonarr/settings#custom-formats-2",
"negate": false,
"required": true,
"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
}
]
},
{
"name": "4k",
"implementation": "ResolutionSpecification",
"implementationName": "Resolution",
"infoLink": "https://wiki.servarr.com/sonarr/settings#custom-formats-2",
"negate": true,
"required": true,
"fields": [
{
"order": 0,
"name": "value",
"label": "Resolution",
"value": 2160,
"type": "select",
"advanced": false,
"selectOptions": [
{
"value": 0,
"name": "Unknown",
"order": 0
},
{
"value": 360,
"name": "R360P",
"order": 360
},
{
"value": 480,
"name": "R480P",
"order": 480
},
{
"value": 540,
"name": "R540p",
"order": 540
},
{
"value": 576,
"name": "R576p",
"order": 576
},
{
"value": 720,
"name": "R720p",
"order": 720
},
{
"value": 1080,
"name": "R1080p",
"order": 1080
},
{
"value": 2160,
"name": "R2160p",
"order": 2160
}
],
"privacy": "normal",
"isFloat": false
}
]
}
]
},
@@ -10671,5 +10795,349 @@
]
}
]
},
{
"name": "h265 (4k)",
"includeCustomFormatWhenRenaming": false,
"specifications": [
{
"name": "h265",
"implementation": "ReleaseTitleSpecification",
"implementationName": "Release Title",
"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": "(?i)h\\s*\\.?\\s*265",
"type": "textbox",
"advanced": false,
"privacy": "normal",
"isFloat": false
}
]
},
{
"name": "Disc",
"implementation": "ReleaseTitleSpecification",
"implementationName": "Release Title",
"infoLink": "https://wiki.servarr.com/sonarr/settings#custom-formats-2",
"negate": true,
"required": true,
"fields": [
{
"order": 0,
"name": "value",
"label": "Language",
"helpText": "Custom Format RegEx is Case Insensitive",
"value": "^(?!.*\\b((?<!HD[._ -]|HD)DVD|BDRip|MKV|XviD|WMV|d3g|BDREMUX|REMUX|^(?=.*1080p)(?=.*HEVC)|[xh][-_. ]?26[45]|German.*DL|((?<=\\d{4}).*German.*(DL)?)(?=.*\\b(AVC|HEVC|VC[-_. ]?1|MVC|MPEG[-_. ]?2)\\b))\\b)(((?=.*\\b(Blu[-_. ]?ray|BD|HD[-_. ]?DVD)\\b)(?=.*\\b(AVC|HEVC|VC[-_. ]?1|MVC|MPEG[-_. ]?2|BDMV|ISO)\\b))|^((?=.*\\b(^((?=.*\\b((.*_)?COMPLETE.*|Dis[ck])\\b)(?=.*(Blu[-_. ]?ray|HD[-_. ]?DVD)))|3D[-_. ]?BD|BR[-_. ]?DISK|Full[-_. ]?Blu[-_. ]?ray|^((?=.*((BD|UHD)[-_. ]?(25|50|66|100|ISO)))))))).*|(?i)(DVD9|DVD5|NTSC|PAL|VOB IFO|VC-1|AVC|MPEG-2|\\bCOMPLETE[-.\\s]?(?:UHD[-.\\s])?BLU[-.\\s]?RAY\\b|\\bCOMPLETE BLURAY\\b|\\bBR-Disk\\b)",
"type": "textbox",
"advanced": false,
"privacy": "normal",
"isFloat": false
}
]
},
{
"name": "Remux",
"implementation": "ReleaseTitleSpecification",
"implementationName": "Release Title",
"infoLink": "https://wiki.servarr.com/sonarr/settings#custom-formats-2",
"negate": true,
"required": true,
"fields": [
{
"order": 0,
"name": "value",
"label": "Language",
"helpText": "Custom Format RegEx is Case Insensitive",
"value": "Remux",
"type": "textbox",
"advanced": false,
"privacy": "normal",
"isFloat": false
}
]
},
{
"name": "WEB",
"implementation": "SourceSpecification",
"implementationName": "Source",
"infoLink": "https://wiki.servarr.com/sonarr/settings#custom-formats-2",
"negate": false,
"required": true,
"fields": [
{
"order": 0,
"name": "value",
"label": "Source",
"value": 7,
"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
}
]
},
{
"name": "4k",
"implementation": "ResolutionSpecification",
"implementationName": "Resolution",
"infoLink": "https://wiki.servarr.com/sonarr/settings#custom-formats-2",
"negate": false,
"required": true,
"fields": [
{
"order": 0,
"name": "value",
"label": "Resolution",
"value": 2160,
"type": "select",
"advanced": false,
"selectOptions": [
{
"value": 0,
"name": "Unknown",
"order": 0
},
{
"value": 360,
"name": "R360P",
"order": 360
},
{
"value": 480,
"name": "R480P",
"order": 480
},
{
"value": 540,
"name": "R540p",
"order": 540
},
{
"value": 576,
"name": "R576p",
"order": 576
},
{
"value": 720,
"name": "R720p",
"order": 720
},
{
"value": 1080,
"name": "R1080p",
"order": 1080
},
{
"value": 2160,
"name": "R2160p",
"order": 2160
}
],
"privacy": "normal",
"isFloat": false
}
]
}
]
},
{
"name": "MAX",
"includeCustomFormatWhenRenaming": true,
"specifications": [
{
"name": "Max",
"implementation": "ReleaseTitleSpecification",
"implementationName": "Release Title",
"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": "\\b((?<!hbo[ ._-])max)\\b(?=[ ._-]web[ ._-]?(dl|rip)\\b)",
"type": "textbox",
"advanced": false,
"privacy": "normal",
"isFloat": false
}
]
},
{
"name": "WEBDL",
"implementation": "SourceSpecification",
"implementationName": "Source",
"infoLink": "https://wiki.servarr.com/sonarr/settings#custom-formats-2",
"negate": false,
"required": false,
"fields": [
{
"order": 0,
"name": "value",
"label": "Source",
"value": 7,
"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
}
]
},
{
"name": "WEBRIP",
"implementation": "SourceSpecification",
"implementationName": "Source",
"infoLink": "https://wiki.servarr.com/sonarr/settings#custom-formats-2",
"negate": false,
"required": false,
"fields": [
{
"order": 0,
"name": "value",
"label": "Source",
"value": 8,
"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
}
]
}
]
}
]

164
deletarr.py Normal file
View File

@@ -0,0 +1,164 @@
import requests
import os
import yaml
import json
# ANSI escape sequences for colors
class Colors:
HEADER = '\033[95m' # Purple for questions and headers
OKBLUE = '\033[94m' # Blue for actions
OKGREEN = '\033[92m' # Green for success messages
FAIL = '\033[91m' # Red for error messages
ENDC = '\033[0m' # Reset to default
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
# Load configuration for main app
with open('config.yml', 'r') as config_file:
config = yaml.safe_load(config_file)
master_config = config['instances']['master']
def print_success(message):
print(Colors.OKGREEN + message + Colors.ENDC)
def print_error(message):
print(Colors.FAIL + message + Colors.ENDC)
def print_connection_error():
print(Colors.FAIL + "Failed to connect to the service! Please check if it's running and accessible." + Colors.ENDC)
def get_user_choice():
print(Colors.HEADER + "\nAvailable instances to delete from:" + Colors.ENDC)
sources = []
# Add master installations
for app in master_config:
sources.append((app, f"{app.capitalize()} [Master]"))
# Add extra installations
if "extras" in config['instances']:
for app, instances in config['instances']['extras'].items():
for install in instances:
sources.append((app, f"{app.capitalize()} [{install['name']}]"))
# Display sources with numbers
for idx, (app, name) in enumerate(sources, start=1):
print(f"{idx}. {name}")
# User selection
choice = input(Colors.HEADER + "Enter the number of the instance to delete from: " + Colors.ENDC).strip()
while not choice.isdigit() or int(choice) < 1 or int(choice) > len(sources):
print_error("Invalid input. Please enter a valid number.")
choice = input(Colors.HEADER + "Enter the number of the instance to delete from: " + Colors.ENDC).strip()
selected_app, selected_name = sources[int(choice) - 1]
print()
return selected_app, selected_name
def user_select_items_to_delete(items):
print(Colors.HEADER + "\nAvailable items:" + Colors.ENDC)
for idx, item in enumerate(items, start=1):
print(f"{idx}. {item['name']}")
print(Colors.HEADER + "Type the number(s) of the items you wish to delete separated by commas, or type 'all' to delete everything." + Colors.ENDC)
selection = input(Colors.HEADER + "Your choice: " + Colors.ENDC).strip().lower()
if selection == 'all':
return [item['id'] for item in items] # Return all IDs if "all" is selected
else:
selected_ids = []
try:
selected_indices = [int(i) - 1 for i in selection.split(',') if i.isdigit()]
for idx in selected_indices:
if idx < len(items):
selected_ids.append(items[idx]['id'])
return selected_ids
except ValueError:
print_error("Invalid input. Please enter a valid number or 'all'.")
return []
def delete_custom_formats(source_config):
print(Colors.OKBLUE + "\nDeleting selected custom formats..." + Colors.ENDC)
headers = {"X-Api-Key": source_config['api_key']}
get_url = f"{source_config['base_url']}/api/v3/customformat"
try:
response = requests.get(get_url, headers=headers)
if response.status_code == 200:
formats_to_delete = response.json()
selected_ids = user_select_items_to_delete(formats_to_delete)
for format_id in selected_ids:
delete_url = f"{get_url}/{format_id}"
del_response = requests.delete(delete_url, headers=headers)
format_name = next((item['name'] for item in formats_to_delete if item['id'] == format_id), "Unknown")
if del_response.status_code in [200, 202, 204]:
print(Colors.OKBLUE + f"Deleting custom format '{format_name}': " + Colors.ENDC + Colors.OKGREEN + "SUCCESS" + Colors.ENDC)
else:
print(Colors.OKBLUE + f"Deleting custom format '{format_name}': " + Colors.ENDC + Colors.FAIL + "FAIL" + Colors.ENDC)
else:
print_error("Failed to retrieve custom formats for deletion!")
except requests.exceptions.ConnectionError:
print_connection_error()
def delete_quality_profiles(source_config):
print(Colors.OKBLUE + "\nDeleting selected quality profiles..." + Colors.ENDC)
headers = {"X-Api-Key": source_config['api_key']}
get_url = f"{source_config['base_url']}/api/v3/qualityprofile"
try:
response = requests.get(get_url, headers=headers)
if response.status_code == 200:
profiles_to_delete = response.json()
selected_ids = user_select_items_to_delete(profiles_to_delete)
for profile_id in selected_ids:
delete_url = f"{get_url}/{profile_id}"
del_response = requests.delete(delete_url, headers=headers)
profile_name = next((item['name'] for item in profiles_to_delete if item['id'] == profile_id), "Unknown")
if del_response.status_code in [200, 202, 204]:
print(Colors.OKBLUE + f"Deleting quality profile '{profile_name}': " + Colors.ENDC + Colors.OKGREEN + "SUCCESS" + Colors.ENDC)
else:
# Handle failure due to the profile being in use or other errors
error_message = "Failed to delete due to an unknown error."
try:
# Attempt to parse JSON error message from response
error_details = del_response.json()
if 'message' in error_details:
error_message = error_details['message']
elif 'error' in error_details:
error_message = error_details['error']
except json.JSONDecodeError:
# If response is not JSON or doesn't have expected fields
error_message = del_response.text or "Failed to delete with no detailed error message."
print(Colors.OKBLUE + f"Deleting quality profile '{profile_name}': " + Colors.ENDC + Colors.FAIL + f"FAIL - {error_message}" + Colors.ENDC)
else:
print_error("Failed to retrieve quality profiles for deletion!")
except requests.exceptions.ConnectionError:
print_connection_error()
def get_app_config(app_name, instance_name):
if instance_name.endswith("[Master]"):
return master_config[app_name]
else:
instance_name = instance_name.replace(f"{app_name.capitalize()} [", "").replace("]", "")
extras = config['instances']['extras'].get(app_name, [])
for instance in extras:
if instance['name'] == instance_name:
return instance
raise ValueError(f"Configuration for {app_name} - {instance_name} not found.")
if __name__ == "__main__":
selected_app, selected_instance = get_user_choice()
source_config = get_app_config(selected_app, selected_instance)
source_config['app_name'] = selected_app
print(Colors.HEADER + "\nChoose what to delete:" + Colors.ENDC)
print("1. Custom Formats")
print("2. Quality Profiles")
choice = input(Colors.HEADER + "Enter your choice (1/2): " + Colors.ENDC).strip()
if choice == "1":
delete_custom_formats(source_config)
elif choice == "2":
delete_quality_profiles(source_config)

View File

@@ -0,0 +1,23 @@
version: "3.3"
services:
radarr:
image: linuxserver/radarr
container_name: radarr
environment:
- PUID=1000 # user id, change as necessary
- PGID=1000 # group id, change as necessary
- TZ=Europe/London # timezone, change as necessary
ports:
- "7887:7878" # change the left value to the desired host port for Radarr
restart: unless-stopped
sonarr:
image: linuxserver/sonarr
container_name: sonarr
environment:
- PUID=1000 # user id, change as necessary
- PGID=1000 # group id, change as necessary
- TZ=Europe/London # timezone, change as necessary
ports:
- "8998:8989" # change the left value to the desired host port for Sonarr
restart: unless-stopped

View File

@@ -1,7 +1,8 @@
import json
import requests
import os
import re
import yaml
import json
# ANSI escape sequences for colors
class Colors:
@@ -15,16 +16,39 @@ class Colors:
UNDERLINE = '\033[4m'
# Load configuration for main app
with open('config.json', 'r') as config_file:
config = json.load(config_file)['master']
with open('config.yml', 'r') as config_file:
config = yaml.safe_load(config_file)
master_config = config['instances']['master']
export_base_path = config['settings']['export_path']
def get_user_choice():
choice = input("Enter an app to export from (radarr/sonarr): ").lower()
while choice not in ["radarr", "sonarr"]:
print(Colors.FAIL + "Invalid input. Please enter either 'radarr' or 'sonarr'." + Colors.ENDC)
choice = input("Enter the source (radarr/sonarr): ").lower()
sources = []
print(Colors.HEADER + "Available sources to export from:" + Colors.ENDC)
# Add master installations
for app in master_config:
sources.append((app, f"{app.capitalize()} [Master]", "master"))
# Add extra installations
if "extras" in config['instances']:
for app, instances in config['instances']['extras'].items():
for install in instances:
sources.append((app, f"{app.capitalize()} [{install['name']}]", install['name']))
# Display sources with numbers
for idx, (app, name, _) in enumerate(sources, start=1):
print(f"{idx}. {name}")
# User selection
choice = input("Enter the number of the app to export from: ").strip()
while not choice.isdigit() or int(choice) < 1 or int(choice) > len(sources):
print(Colors.FAIL + "Invalid input. Please enter a valid number." + Colors.ENDC)
choice = input("Enter the number of the app to export from: ").strip()
selected_app, instance_name = sources[int(choice) - 1][0], sources[int(choice) - 1][2]
print()
return choice
return selected_app, instance_name
def get_export_choice():
print(Colors.HEADER + "Choose what to export:" + Colors.ENDC)
@@ -39,7 +63,7 @@ def get_export_choice():
return choice
def get_app_config(source):
app_config = config[source]
app_config = master_config[source]
return app_config['base_url'], app_config['api_key']
def sanitize_filename(filename):
@@ -70,8 +94,10 @@ def ensure_directory_exists(directory):
os.makedirs(directory)
print(Colors.OKBLUE + f"Created directory: {directory}" + Colors.ENDC)
def export_cf(source, save_path='./custom_formats'):
ensure_directory_exists(save_path) # Ensure the directory exists with the given save_path
def export_cf(source, instance_name, save_path=None):
if save_path is None:
save_path = os.path.join(export_base_path, source, instance_name, 'custom_formats')
ensure_directory_exists(save_path)
base_url, api_key = get_app_config(source)
headers = {"X-Api-Key": api_key}
@@ -93,7 +119,7 @@ def export_cf(source, save_path='./custom_formats'):
custom_format.pop('id', None)
saved_formats.append(custom_format['name'])
file_path = f'{save_path}/Custom Formats ({source.capitalize()}).json'
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)
@@ -108,8 +134,10 @@ def export_cf(source, save_path='./custom_formats'):
def export_qf(source, save_path='./profiles'):
ensure_directory_exists(save_path) # Ensure the directory exists with the given save_path
def export_qf(source, instance_name, save_path=None):
if save_path is None:
save_path = os.path.join(export_base_path, source, instance_name, 'profiles')
ensure_directory_exists(save_path)
base_url, api_key = get_app_config(source)
headers = {"X-Api-Key": api_key}
@@ -124,9 +152,6 @@ def export_qf(source, save_path='./profiles'):
quality_profiles = response.json()
print(Colors.OKGREEN + f"Found {len(quality_profiles)} quality profiles." + Colors.ENDC)
if not os.path.exists('./profiles'):
os.makedirs('./profiles')
saved_profiles = []
for profile in quality_profiles:
profile.pop('id', None)
@@ -134,12 +159,12 @@ def export_qf(source, save_path='./profiles'):
profile_name = sanitize_filename(profile_name)
profile_filename = f"{profile_name} ({source.capitalize()}).json"
profile_filepath = os.path.join(save_path, profile_filename)
saved_profiles.append(profile_name)
with open(profile_filepath, 'w') as file:
json.dump([profile], file, indent=4)
saved_profiles.append(profile_name) # Add the profile name to the list
print_saved_items(saved_profiles, "Quality Profiles")
print(Colors.OKGREEN + f"Saved to '{profile_filepath}'" + Colors.ENDC)
print(Colors.OKGREEN + f"Saved to '{os.path.normpath(save_path)}'" + Colors.ENDC) # Normalize the path
print()
else:
handle_response_errors(response)
@@ -149,10 +174,10 @@ def export_qf(source, save_path='./profiles'):
if __name__ == "__main__":
user_choice = get_user_choice()
user_choice, instance_name = get_user_choice()
export_choice = get_export_choice()
if export_choice in ["1", "3"]:
export_cf(user_choice)
export_cf(user_choice, instance_name)
if export_choice in ["2", "3"]:
export_qf(user_choice)
export_qf(user_choice, instance_name)

View File

@@ -1,6 +1,8 @@
import json
import requests
import os
import re
import yaml
import json
# ANSI escape sequences for colors
class Colors:
@@ -14,8 +16,9 @@ class Colors:
UNDERLINE = '\033[4m'
# Load configuration for main app
with open('config.json', 'r') as config_file:
config = json.load(config_file)
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)
@@ -27,11 +30,32 @@ 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():
choice = input("Enter the app you want to import to (radarr/sonarr): ").lower()
while choice not in ["radarr", "sonarr"]:
print_error("Invalid input. Please enter either 'radarr' or 'sonarr'.")
choice = input("Enter the source (radarr/sonarr): ").lower()
return 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()
@@ -44,67 +68,93 @@ def get_import_choice():
choice = input("Enter your choice (1/2): ").strip()
return choice
def get_app_config(source):
return config['master'][source]
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
def select_file(directory):
files = [f for f in os.listdir(directory) if os.path.isfile(os.path.join(directory, f))]
print()
print(Colors.OKBLUE + "Available files:" + Colors.ENDC)
for i, file in enumerate(files, 1):
print(f"{i}. {file}")
choice = int(input("Select a file to import: "))
return files[choice - 1]
def import_custom_formats(source_config, import_path='./custom_formats', auto_select_file=False):
choice = input("Select a file to import (or 'all' for all files): ").strip()
print()
if choice.isdigit() and 1 <= int(choice) <= len(files):
return [files[int(choice) - 1]]
elif choice.lower() == 'all':
return files
else:
print_error("Invalid input. Please enter a valid number or 'all'.")
print()
return None
def import_custom_formats(source_config, import_path='./custom_formats', selected_files=None, sync_mode=False):
headers = {"X-Api-Key": source_config['api_key']}
get_url = f"{source_config['base_url']}/api/v3/customformat"
try:
response = requests.get(get_url, headers=headers)
if response.status_code == 200:
existing_formats = response.json()
existing_names_to_id = {format['name']: format['id'] for format in existing_formats}
files = os.listdir(import_path)
if auto_select_file and len(files) == 1:
selected_file = files[0]
else:
selected_file = select_file(import_path)
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
added_count, updated_count = 0, 0
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)
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())
print()
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())
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())
else:
post_url = f"{source_config['base_url']}/api/v3/customformat"
response = requests.post(post_url, json=format, headers=headers)
if response.status_code in [200, 201]:
print(Colors.OKBLUE + f"Adding custom format '{format_name}': " + Colors.ENDC, end='')
print_success("SUCCESS")
added_count += 1
else:
print_error(f"Adding custom format '{format_name}': FAIL")
print(response.content.decode())
print()
print_success(f"Successfully added {added_count} custom formats, updated {updated_count} custom formats.")
print()
print_success(f"Successfully added {added_count} custom formats, updated {updated_count} custom formats.")
else:
print_error(f"Failed to retrieve existing custom formats from {get_url}! (HTTP {response.status_code})")
@@ -113,35 +163,24 @@ def import_custom_formats(source_config, import_path='./custom_formats', auto_se
except requests.exceptions.ConnectionError:
print_connection_error()
def import_quality_profiles(source_config, import_path='./profiles'):
def import_quality_profiles(source_config, import_path='./profiles', selected_files=None, sync_mode=False):
headers = {"X-Api-Key": source_config['api_key']}
try:
cf_import_sync(source_config)
profile_dir = import_path
profiles = [f for f in os.listdir(profile_dir) if f.endswith('.json')]
if not selected_files:
if sync_mode:
# Automatically select all profile files
selected_files = [f for f in os.listdir(import_path) if os.path.isfile(os.path.join(import_path, f))]
print()
print(Colors.HEADER + "Available Profiles:" + Colors.ENDC)
for i, profile in enumerate(profiles, 1):
print(f"{i}. {profile}")
print(f"{len(profiles) + 1}. Import all profiles")
print()
selection = input("Please enter the number of the profile you want to import (or enter " + str(len(profiles) + 1) + " to import all): ")
selected_files = []
try:
selection = int(selection)
if selection == len(profiles) + 1:
selected_files = profiles
else:
selected_files = [profiles[selection - 1]]
except (ValueError, IndexError):
print_error("Invalid selection, please enter a valid number.")
return
if not selected_files:
return # Exit if no file is selected
for selected_file in selected_files:
with open(os.path.join(profile_dir, selected_file), 'r') as file:
with open(os.path.join(import_path, selected_file), 'r') as file:
try:
quality_profiles = json.load(file)
except json.JSONDecodeError as e:
@@ -187,6 +226,8 @@ def import_quality_profiles(source_config, import_path='./profiles'):
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"
@@ -209,11 +250,16 @@ def cf_import_sync(source_config):
if __name__ == "__main__":
user_choice = get_user_choice()
source_config = get_app_config(user_choice)
selected_app, selected_instance = get_user_choice()
source_config = get_app_config(selected_app, selected_instance)
source_config['app_name'] = selected_app
import_choice = get_import_choice()
if import_choice == "1":
import_custom_formats(source_config)
selected_files = select_file('./custom_formats', selected_app)
if selected_files:
import_custom_formats(source_config, './custom_formats', selected_files)
elif import_choice == "2":
import_quality_profiles(source_config)
selected_files = select_file('./profiles', selected_app)
if selected_files:
import_quality_profiles(source_config, './profiles', selected_files)

BIN
requirements.txt Normal file

Binary file not shown.

25
setup.py Normal file
View File

@@ -0,0 +1,25 @@
config_content = """
instances:
master:
sonarr:
base_url: "http://localhost:8989"
api_key: "API_KEY"
radarr:
base_url: "http://localhost:7878"
api_key: "API_KEY"
extras:
sonarr:
- name: "4k-sonarr"
base_url: "http://localhost:8998"
api_key: "API_KEY"
radarr:
- name: "4k-radarr"
base_url: "http://localhost:7887"
api_key: "API_KEY"
settings:
export_path: "./exports"
"""
with open('config.yml', 'w') as file:
file.write(config_content)

View File

@@ -1,33 +1,40 @@
import exportarr
import importarr
import yaml
import json
import shutil
import os
import exportarr # Assuming this module contains the export functions
import importarr # Assuming this module contains the import functions
def sync_data():
# Load configuration for main app
with open('config.json', 'r') as config_file:
config = json.load(config_file)
def sync_data(sync_mode=False):
# Load configuration from YAML file
with open('config.yml', 'r') as config_file:
config = yaml.safe_load(config_file)
# Specify the temporary path where files were saved
# Specify the temporary path where files will be saved
temp_cf_path = './temp_directory/custom_formats'
temp_qf_path = './temp_directory/quality_profiles'
# Get user choice for app (radarr/sonarr)
app_choice = importarr.get_user_choice()
# Export data for the chosen app
exportarr.export_cf(app_choice, save_path=temp_cf_path)
exportarr.export_qf(app_choice, save_path=temp_qf_path)
app_choice = input("Select the app you want to sync:\n1. Radarr\n2. Sonarr\nEnter your choice (1 or 2): ").strip()
while app_choice not in ["1", "2"]:
print("Invalid input. Please enter 1 for Radarr or 2 for Sonarr.")
app_choice = input("Enter your choice (1 or 2): ").strip()
app_choice = "radarr" if app_choice == "1" else "sonarr"
instance_name = "temp"
exportarr.export_cf(app_choice, instance_name, save_path=temp_cf_path)
exportarr.export_qf(app_choice, instance_name, save_path=temp_qf_path)
# Sync with each extra installation of the chosen app
for extra_instance in config['extra_installations'].get(app_choice, []):
source_config = extra_instance
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
importarr.import_custom_formats(source_config, import_path=temp_cf_path, auto_select_file=True)
importarr.import_quality_profiles(source_config, import_path=temp_qf_path)
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'
@@ -36,4 +43,5 @@ def sync_data():
print(f"Deleted temporary directory: {temp_directory}")
if __name__ == "__main__":
sync_data()
# Set sync_mode to True to enable automatic selection of all files during import
sync_data(sync_mode=True)