commit
43ec1c224a
@ -0,0 +1,10 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
|
||||||
|
dev-config.conf
|
||||||
|
output.txt
|
@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: Junkbite
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**Version**
|
||||||
|
Syncarr version and if using docker or config file
|
||||||
|
|
||||||
|
**Config**
|
||||||
|
Your config file and what versions of sonarr/radarr/lidarr you are using
|
||||||
|
|
||||||
|
**Debug logs**
|
||||||
|
Enable debugging with
|
||||||
|
```int
|
||||||
|
[general]
|
||||||
|
log_level=10
|
||||||
|
|
||||||
|
or for docker LOG_LEVEL=10
|
||||||
|
```
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: 'enhancement'
|
||||||
|
assignees: 'Junkbite'
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
@ -0,0 +1,10 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
|
||||||
|
dev-config.conf
|
||||||
|
output.txt
|
@ -0,0 +1,15 @@
|
|||||||
|
FROM python:3.6
|
||||||
|
|
||||||
|
ENV IS_IN_DOCKER 1
|
||||||
|
|
||||||
|
# default every 5 minutes
|
||||||
|
ENV SYNC_INTERVAL_SECONDS 300
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt requirements.txt
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD ["python", "/app/index.py"]
|
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 syncarr
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
@ -0,0 +1,176 @@
|
|||||||
|
# Syncarr
|
||||||
|
Syncs two Radarr/Sonarr/Lidarr servers through the web API. Useful for syncing a 4k radarr/sonarr instance to a 1080p radarr/sonarr instance.
|
||||||
|
|
||||||
|
* Supports Radarr/Sonarr version 2 and 3.
|
||||||
|
* Can sync by `profile` name or `profile_id`
|
||||||
|
* Filter what media gets synced by `profile` name or `profile_id`
|
||||||
|
* Supports Docker for multiple instances
|
||||||
|
* Can set interval for syncing
|
||||||
|
* Support two way sync (one way by default)
|
||||||
|
* Set language profiles (Sonarr v3 only)
|
||||||
|
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
1. Edit the config.conf file and enter your servers URLs and API keys for each server.
|
||||||
|
2. Add the profile name (case insensitive) and movie path for the radarr instance the movies will be synced to:
|
||||||
|
```ini
|
||||||
|
[radarrA]
|
||||||
|
url = https://4k.example.com:443
|
||||||
|
key = XXXXX
|
||||||
|
|
||||||
|
[radarrB]
|
||||||
|
url = http://127.0.0.1:8080
|
||||||
|
key = XXXXX
|
||||||
|
profile = 1080p
|
||||||
|
path = /data/Movies
|
||||||
|
```
|
||||||
|
3. Or if you want to sync two sonarr instances:
|
||||||
|
```ini
|
||||||
|
[sonarrA]
|
||||||
|
url = https://4k.example.com:443
|
||||||
|
key = XXXXX
|
||||||
|
|
||||||
|
[sonarrB]
|
||||||
|
url = http://127.0.0.1:8080
|
||||||
|
key = XXXXX
|
||||||
|
profile = 1080p
|
||||||
|
path = /data/Shows
|
||||||
|
|
||||||
|
4. Or if you want to sync two lidarr instances:
|
||||||
|
```ini
|
||||||
|
[lidarrA]
|
||||||
|
url = https://lossless.example.com:443
|
||||||
|
key = XXXXX
|
||||||
|
|
||||||
|
[lidarrB]
|
||||||
|
url = http://127.0.0.1:8080
|
||||||
|
key = XXXXX
|
||||||
|
profile = Standard
|
||||||
|
path = /data/Music
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note** you cannot have a mix of radarr, lidarr, or sonarr config setups at the same time.
|
||||||
|
|
||||||
|
5. Optional Configuration
|
||||||
|
```ini
|
||||||
|
[sonarrA]
|
||||||
|
url = http://127.0.0.1:8080
|
||||||
|
key = XXXXX
|
||||||
|
profile_filter = 1080p # add a filter to only sync contents belonging to this profile (can set by profile_filter_id as well)
|
||||||
|
|
||||||
|
[sonarrB]
|
||||||
|
url = http://127.0.0.1:8080
|
||||||
|
key = XXXXX
|
||||||
|
profile_id = 1 # Syncarr will try to find id from name but you can specify the id directly if you want
|
||||||
|
language = Vietnamese # can set language for new content added (Sonarr v3 only) (can set by language_id as well)
|
||||||
|
path = /data/Movies
|
||||||
|
|
||||||
|
[general]
|
||||||
|
sync_bidirectionally = 1 # sync from instance A to B **AND** instance B to A
|
||||||
|
auto_search = 0 # search is automatically started on new content - disable by setting to 0
|
||||||
|
monitor_new_content = 0 # set to 0 to never monitor new content synced or to 1 to always monitor new content synced
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note** If `sync_bidirectionally` is set to `1`, then instance A will require either `profile_id` or `profile` AND `path` as well
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
* Python 3.6 or greater
|
||||||
|
* 2x Radarr, Sonarr, or Lidarr servers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to Run
|
||||||
|
1. install the needed python modules (you'll need pip or you can install the modules manually inside the `requirements.txt` file):
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
2. run this script directly or through a Cron:
|
||||||
|
```bash
|
||||||
|
python index.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
## Docker Compose
|
||||||
|
This script can run through a docker container with a set interval (default every 5 minutes)
|
||||||
|
|
||||||
|
```
|
||||||
|
syncarr:
|
||||||
|
image: syncarr/syncarr:latest
|
||||||
|
container_name: syncarr
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
RADARR_A_URL: https://4k.example.com:443
|
||||||
|
RADARR_A_KEY: XXXXX
|
||||||
|
RADARR_B_URL: http://127.0.0.1:8080
|
||||||
|
RADARR_B_KEY: XXXXX
|
||||||
|
RADARR_B_PROFILE: 1080p
|
||||||
|
RADARR_B_PATH: /data/Movies
|
||||||
|
SYNC_INTERVAL_SECONDS: 300
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```
|
||||||
|
syncarr:
|
||||||
|
image: syncarr/syncarr:latest
|
||||||
|
container_name: syncarr
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
SONARR_A_URL: https://4k.example.com:443
|
||||||
|
SONARR_A_KEY: XXXXX
|
||||||
|
SONARR_B_URL: http://127.0.0.1:8080
|
||||||
|
SONARR_B_KEY: XXXXX
|
||||||
|
SONARR_B_PROFILE: 1080p
|
||||||
|
SONARR_B_PATH: /data/Shows
|
||||||
|
SYNC_INTERVAL_SECONDS: 300
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```
|
||||||
|
syncarr:
|
||||||
|
image: syncarr/syncarr:latest
|
||||||
|
container_name: syncarr
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
LIDARR_A_URL: https://lossless.example.com:443
|
||||||
|
LIDARR_A_KEY: XXXXX
|
||||||
|
LIDARR_B_URL: http://127.0.0.1:8080
|
||||||
|
LIDARR_B_KEY: XXXXX
|
||||||
|
LIDARR_B_PROFILE: Standard
|
||||||
|
LIDARR_B_PATH: /data/Music
|
||||||
|
SYNC_INTERVAL_SECONDS: 300
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
## Docker
|
||||||
|
For just plain docker (radarr example):
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run -it --rm --name syncarr -e RADARR_A_URL=https://4k.example.com:443 -e RADARR_A_KEY=XXXXX -e RADARR_B_URL=http://127.0.0.1:8080 -e RADARR_B_KEY=XXXXX -e RADARR_B_PROFILE=1080p -e RADARR_B_PATH=/data/Movies -e SYNC_INTERVAL_SECONDS=300 syncarr/syncarr
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes**
|
||||||
|
* You can also specify the `PROFILE_ID` directly through the `*ARR_A_PROFILE_ID` and `*ARR_B_PROFILE_ID` ENV variables.
|
||||||
|
To filter by profile in docker use `ARR_A_PROFILE_FILTER` or `ARR_A_PROFILE_FILTER_ID` ENV variables. (same for `*arr_B` in bidirectional sync)
|
||||||
|
* Language for new content (Sonarr v3 only) can be set by `SONARR_B_LANGUAGE` or `SONARR_B_LANGUAGE_ID` (and `SONARR_B` if bidirectional sync)
|
||||||
|
* Set bidirectional sync with `SYNCARR_BIDIRECTIONAL_SYNC=1`
|
||||||
|
* Set disable auto searching on new content with `SYNCARR_AUTO_SEARCH=0`
|
||||||
|
* Set if you wanted to monitor new content with `SYNCARR_MONITOR_NEW_CONTENT=1/0`
|
||||||
|
|
||||||
|
---
|
||||||
|
## Troubleshooting
|
||||||
|
If you need to troubleshoot syncarr, then you can either set the log level through the config file:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[general]
|
||||||
|
log_level = 10
|
||||||
|
```
|
||||||
|
|
||||||
|
Or in docker, set the `LOG_LEVEL` ENV variable. Default is set to `20` (info only) but you can set to `10` to get debug info as well. When pasting debug logs online, **make sure to remove any apikeys and any other data you don't want others to see.**
|
||||||
|
|
||||||
|
---
|
||||||
|
## Disclaimer
|
||||||
|
Back up your instances before trying this out. I am not responsible for any lost data.
|
@ -0,0 +1,12 @@
|
|||||||
|
# [radarrA]
|
||||||
|
# url = https://example.com:443
|
||||||
|
# key = FCKGW-RHQQ2-YXRKT-8TG6W-2B7Q8
|
||||||
|
|
||||||
|
# [radarrB]
|
||||||
|
# url = http://127.0.0.1:8080
|
||||||
|
# key = FCKGW-RHQQ2-YXRKT-8TG6W-2B7Q8
|
||||||
|
# profile_id = 1
|
||||||
|
# path = /data/4k_Movies
|
||||||
|
|
||||||
|
# [general]
|
||||||
|
# sync_bidirectionally = 0
|
@ -0,0 +1,439 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import configparser
|
||||||
|
|
||||||
|
|
||||||
|
DEV = os.environ.get('DEV')
|
||||||
|
VER = '1.7.6'
|
||||||
|
DEBUG_LINE = '-' * 20
|
||||||
|
|
||||||
|
V1_API_PATH = 'v1/'
|
||||||
|
V2_API_PATH = ''
|
||||||
|
V3_API_PATH = 'v3/'
|
||||||
|
|
||||||
|
# https://github.com/lidarr/Lidarr/wiki/Artist
|
||||||
|
# https://github.com/Radarr/Radarr/wiki/API:Movie
|
||||||
|
# https://github.com/Sonarr/Sonarr/wiki/Series
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
# get docker based ENV vars
|
||||||
|
is_in_docker = os.environ.get('IS_IN_DOCKER')
|
||||||
|
instance_sync_interval_seconds = os.environ.get('SYNC_INTERVAL_SECONDS')
|
||||||
|
if instance_sync_interval_seconds:
|
||||||
|
instance_sync_interval_seconds = int(instance_sync_interval_seconds)
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
|
||||||
|
def ConfigSectionMap(section):
|
||||||
|
'''get all config options from config file'''
|
||||||
|
dict1 = {}
|
||||||
|
options = config.options(section)
|
||||||
|
for option in options:
|
||||||
|
try:
|
||||||
|
dict1[option] = config.get(section, option)
|
||||||
|
except:
|
||||||
|
print("exception on %s!" % option)
|
||||||
|
dict1[option] = None
|
||||||
|
return dict1
|
||||||
|
|
||||||
|
|
||||||
|
def get_config_value(env_key, config_key, config_section):
|
||||||
|
if is_in_docker:
|
||||||
|
value = os.environ.get(env_key)
|
||||||
|
if value is not None:
|
||||||
|
return value
|
||||||
|
|
||||||
|
try:
|
||||||
|
_config = ConfigSectionMap(config_section)
|
||||||
|
return _config.get(config_key)
|
||||||
|
except configparser.NoSectionError:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
# load config file
|
||||||
|
|
||||||
|
|
||||||
|
BASE_CONFIG = 'config.conf'
|
||||||
|
if DEV:
|
||||||
|
settingsFilename = os.path.join(os.getcwd(), 'dev-{}'.format(BASE_CONFIG))
|
||||||
|
else:
|
||||||
|
settingsFilename = os.path.join(os.getcwd(), BASE_CONFIG)
|
||||||
|
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config.read(settingsFilename)
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
# get config settings from ENV or config files for Radarr
|
||||||
|
radarrA_url = get_config_value('RADARR_A_URL', 'url', 'radarrA')
|
||||||
|
radarrA_key = get_config_value('RADARR_A_KEY', 'key', 'radarrA')
|
||||||
|
radarrA_profile = get_config_value('RADARR_A_PROFILE', 'profile', 'radarrA')
|
||||||
|
radarrA_profile_id = get_config_value(
|
||||||
|
'RADARR_A_PROFILE_ID', 'profile_id', 'radarrA')
|
||||||
|
radarrA_profile_filter = get_config_value(
|
||||||
|
'RADARR_A_PROFILE_FILTER', 'profile_filter', 'radarrA')
|
||||||
|
radarrA_profile_filter_id = get_config_value(
|
||||||
|
'RADARR_A_PROFILE_FILTER_ID', 'profile_filter_id', 'radarrA')
|
||||||
|
radarrA_path = get_config_value('RADARR_A_PATH', 'path', 'radarrA')
|
||||||
|
|
||||||
|
radarrB_url = get_config_value('RADARR_B_URL', 'url', 'radarrB')
|
||||||
|
radarrB_key = get_config_value('RADARR_B_KEY', 'key', 'radarrB')
|
||||||
|
radarrB_profile = get_config_value('RADARR_B_PROFILE', 'profile', 'radarrB')
|
||||||
|
radarrB_profile_id = get_config_value(
|
||||||
|
'RADARR_B_PROFILE_ID', 'profile_id', 'radarrB')
|
||||||
|
radarrB_profile_filter = get_config_value(
|
||||||
|
'RADARR_B_PROFILE_FILTER', 'profile_filter', 'radarrB')
|
||||||
|
radarrB_profile_filter_id = get_config_value(
|
||||||
|
'RADARR_B_PROFILE_FILTER_ID', 'profile_filter_id', 'radarrB')
|
||||||
|
radarrB_path = get_config_value('RADARR_B_PATH', 'path', 'radarrB')
|
||||||
|
|
||||||
|
# get config settings from ENV or config files for Sonarr
|
||||||
|
sonarrA_url = get_config_value('SONARR_A_URL', 'url', 'sonarrA')
|
||||||
|
sonarrA_key = get_config_value('SONARR_A_KEY', 'key', 'sonarrA')
|
||||||
|
sonarrA_path = get_config_value('SONARR_A_PATH', 'path', 'sonarrA')
|
||||||
|
sonarrA_profile = get_config_value('SONARR_A_PROFILE', 'profile', 'sonarrA')
|
||||||
|
sonarrA_profile_id = get_config_value(
|
||||||
|
'SONARR_A_PROFILE_ID', 'profile_id', 'sonarrA')
|
||||||
|
sonarrA_profile_filter = get_config_value(
|
||||||
|
'SONARR_A_PROFILE_FILTER', 'profile_filter', 'sonarrA')
|
||||||
|
sonarrA_profile_filter_id = get_config_value(
|
||||||
|
'SONARR_A_PROFILE_FILTER_ID', 'profile_filter_id', 'sonarrA')
|
||||||
|
sonarrA_language = get_config_value('SONARR_A_LANGUAGE', 'language', 'sonarrA')
|
||||||
|
sonarrA_language_id = get_config_value(
|
||||||
|
'SONARR_A_LANGUAGE_ID', 'language_id', 'sonarrA')
|
||||||
|
|
||||||
|
|
||||||
|
sonarrB_url = get_config_value('SONARR_B_URL', 'url', 'sonarrB')
|
||||||
|
sonarrB_key = get_config_value('SONARR_B_KEY', 'key', 'sonarrB')
|
||||||
|
sonarrB_path = get_config_value('SONARR_B_PATH', 'path', 'sonarrB')
|
||||||
|
sonarrB_profile = get_config_value('SONARR_B_PROFILE', 'profile', 'sonarrB')
|
||||||
|
sonarrB_profile_id = get_config_value(
|
||||||
|
'SONARR_B_PROFILE_ID', 'profile_id', 'sonarrB')
|
||||||
|
sonarrB_profile_filter = get_config_value(
|
||||||
|
'SONARR_A_PROFILE_FILTER', 'profile_filter', 'sonarrB')
|
||||||
|
sonarrB_profile_filter_id = get_config_value(
|
||||||
|
'SONARR_A_PROFILE_FILTER_ID', 'profile_filter_id', 'sonarrB')
|
||||||
|
sonarrB_language = get_config_value('SONARR_B_LANGUAGE', 'language', 'sonarrB')
|
||||||
|
sonarrB_language_id = get_config_value(
|
||||||
|
'SONARR_B_LANGUAGE_ID', 'language_id', 'sonarrB')
|
||||||
|
|
||||||
|
# get config settings from ENV or config files for Lidarr
|
||||||
|
lidarrA_url = get_config_value('LIDARR_A_URL', 'url', 'lidarrA')
|
||||||
|
lidarrA_key = get_config_value('LIDARR_A_KEY', 'key', 'lidarrA')
|
||||||
|
lidarrA_profile = get_config_value('LIDARR_A_PROFILE', 'profile', 'lidarrA')
|
||||||
|
lidarrA_profile_id = get_config_value(
|
||||||
|
'LIDARR_A_PROFILE_ID', 'profile_id', 'lidarrA')
|
||||||
|
lidarrA_profile_filter = get_config_value(
|
||||||
|
'LIDARR_A_PROFILE_FILTER', 'profile_filter', 'lidarrA')
|
||||||
|
lidarrA_profile_filter_id = get_config_value(
|
||||||
|
'LIDARR_A_PROFILE_FILTER_ID', 'profile_filter_id', 'lidarrA')
|
||||||
|
lidarrA_path = get_config_value('LIDARR_A_PATH', 'path', 'lidarrA')
|
||||||
|
|
||||||
|
lidarrB_url = get_config_value('LIDARR_B_URL', 'url', 'lidarrB')
|
||||||
|
lidarrB_key = get_config_value('LIDARR_B_KEY', 'key', 'lidarrB')
|
||||||
|
lidarrB_profile = get_config_value('LIDARR_B_PROFILE', 'profile', 'lidarrB')
|
||||||
|
lidarrB_profile_id = get_config_value(
|
||||||
|
'LIDARR_B_PROFILE_ID', 'profile_id', 'lidarrB')
|
||||||
|
lidarrB_profile_filter = get_config_value(
|
||||||
|
'LIDARR_A_PROFILE_FILTER', 'profile_filter', 'lidarrB')
|
||||||
|
lidarrB_profile_filter_id = get_config_value(
|
||||||
|
'LIDARR_A_PROFILE_FILTER_ID', 'profile_filter_id', 'lidarrB')
|
||||||
|
lidarrB_path = get_config_value('LIDARR_B_PATH', 'path', 'lidarrB')
|
||||||
|
|
||||||
|
# get general conf options
|
||||||
|
sync_bidirectionally = get_config_value(
|
||||||
|
'SYNCARR_BIDIRECTIONAL_SYNC', 'bidirectional', 'general')
|
||||||
|
if sync_bidirectionally is not None:
|
||||||
|
sync_bidirectionally = int(sync_bidirectionally)
|
||||||
|
|
||||||
|
auto_search = get_config_value(
|
||||||
|
'SYNCARR_AUTO_SEARCH', 'auto_search', 'general')
|
||||||
|
if auto_search is not None:
|
||||||
|
auto_search = int(auto_search)
|
||||||
|
# set to search if config not set
|
||||||
|
if auto_search is None:
|
||||||
|
auto_search = 1
|
||||||
|
|
||||||
|
monitor_new_content = get_config_value(
|
||||||
|
'SYNCARR_MONITOR_NEW_CONTENT', 'monitor_new_content', 'general')
|
||||||
|
if monitor_new_content is not None:
|
||||||
|
monitor_new_content = int(monitor_new_content)
|
||||||
|
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
# setup logger
|
||||||
|
|
||||||
|
# CRITICAL 50, ERROR 40, WARNING 3, INFO 20, DEBUG 10, NOTSET 0
|
||||||
|
log_level = get_config_value('LOG_LEVEL', 'log_level', 'general') or 20
|
||||||
|
if log_level:
|
||||||
|
log_level = int(log_level)
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
|
logger.setLevel(log_level)
|
||||||
|
|
||||||
|
logFormatter = logging.Formatter(
|
||||||
|
"%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s")
|
||||||
|
|
||||||
|
# log to txt file
|
||||||
|
fileHandler = logging.FileHandler("./output.txt")
|
||||||
|
fileHandler.setFormatter(logFormatter)
|
||||||
|
logger.addHandler(fileHandler)
|
||||||
|
|
||||||
|
# log to std out
|
||||||
|
consoleHandler = logging.StreamHandler(sys.stdout)
|
||||||
|
consoleHandler.setFormatter(logFormatter)
|
||||||
|
logger.addHandler(consoleHandler)
|
||||||
|
|
||||||
|
logger.debug('Syncarr Version {}'.format(VER))
|
||||||
|
logger.info('log level {}'.format(log_level))
|
||||||
|
|
||||||
|
if DEV:
|
||||||
|
logger.info('-----------------DEVV-----------------')
|
||||||
|
########################################################################################################################
|
||||||
|
# make sure we have radarr, lidarr, OR sonarr
|
||||||
|
if (
|
||||||
|
(sonarrA_url and radarrA_url) or
|
||||||
|
(sonarrA_url and radarrB_url) or
|
||||||
|
(sonarrA_url and lidarrA_url) or
|
||||||
|
(sonarrA_url and lidarrB_url) or
|
||||||
|
|
||||||
|
(radarrA_url and lidarrA_url) or
|
||||||
|
(radarrA_url and lidarrB_url) or
|
||||||
|
(radarrB_url and lidarrA_url) or
|
||||||
|
(radarrB_url and lidarrB_url)
|
||||||
|
):
|
||||||
|
logger.error(
|
||||||
|
'cannot have more than one *arr type profile(s) setup at the same time')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
# get generic instanceA/B variables
|
||||||
|
instanceA_url = ''
|
||||||
|
instanceA_key = ''
|
||||||
|
instanceA_path = ''
|
||||||
|
instanceA_profile = ''
|
||||||
|
instanceA_profile_id = ''
|
||||||
|
instanceA_profile_filter = ''
|
||||||
|
instanceA_language_id = ''
|
||||||
|
instanceA_language = ''
|
||||||
|
|
||||||
|
instanceB_url = ''
|
||||||
|
instanceB_key = ''
|
||||||
|
instanceB_path = ''
|
||||||
|
instanceB_profile = ''
|
||||||
|
instanceB_profile_id = ''
|
||||||
|
instanceB_profile_filter = ''
|
||||||
|
instanceB_language_id = ''
|
||||||
|
instanceB_language = ''
|
||||||
|
|
||||||
|
|
||||||
|
api_version = '' # we are going to detect what API version we are on
|
||||||
|
tested_api_version = False # only get api version once
|
||||||
|
|
||||||
|
|
||||||
|
api_content_path = '' # url path to add content
|
||||||
|
api_profile_path = '' # url path to get quality profiles
|
||||||
|
api_status_path = '' # url path to check on server status
|
||||||
|
api_language_path = '' # url to get lanaguge profiles
|
||||||
|
|
||||||
|
is_radarr = False
|
||||||
|
is_sonarr = False
|
||||||
|
is_lidarr = False
|
||||||
|
|
||||||
|
content_id_key = '' # the unique id for a content item
|
||||||
|
|
||||||
|
if radarrA_url and radarrB_url:
|
||||||
|
instanceA_url = radarrA_url
|
||||||
|
instanceA_key = radarrA_key
|
||||||
|
instanceA_profile = radarrA_profile
|
||||||
|
instanceA_profile_id = radarrA_profile_id
|
||||||
|
instanceA_profile_filter = radarrA_profile_filter
|
||||||
|
instanceA_profile_filter_id = radarrA_profile_filter_id
|
||||||
|
instanceA_path = radarrA_path
|
||||||
|
|
||||||
|
instanceB_url = radarrB_url
|
||||||
|
instanceB_key = radarrB_key
|
||||||
|
instanceB_profile = radarrB_profile
|
||||||
|
instanceB_profile_id = radarrB_profile_id
|
||||||
|
instanceB_profile_filter = radarrB_profile_filter
|
||||||
|
instanceB_profile_filter_id = radarrB_profile_filter_id
|
||||||
|
instanceB_path = radarrB_path
|
||||||
|
|
||||||
|
api_version = V2_API_PATH # radarr v2 doesnt have version in api url
|
||||||
|
api_content_path = 'movie'
|
||||||
|
api_profile_path = 'profile'
|
||||||
|
api_status_path = 'system/status'
|
||||||
|
|
||||||
|
content_id_key = 'tmdbId'
|
||||||
|
is_radarr = True
|
||||||
|
|
||||||
|
elif lidarrA_url and lidarrB_url:
|
||||||
|
instanceA_url = lidarrA_url
|
||||||
|
instanceA_key = lidarrA_key
|
||||||
|
instanceA_profile = lidarrA_profile
|
||||||
|
instanceA_profile_id = lidarrA_profile_id
|
||||||
|
instanceA_profile_filter = lidarrA_profile_filter
|
||||||
|
instanceA_profile_filter_id = lidarrA_profile_filter_id
|
||||||
|
instanceA_path = lidarrA_path
|
||||||
|
|
||||||
|
instanceB_url = lidarrB_url
|
||||||
|
instanceB_key = lidarrB_key
|
||||||
|
instanceB_profile = lidarrB_profile
|
||||||
|
instanceB_profile_id = lidarrB_profile_id
|
||||||
|
instanceB_profile_filter = lidarrB_profile_filter
|
||||||
|
instanceB_profile_filter_id = lidarrB_profile_filter_id
|
||||||
|
instanceB_path = lidarrB_path
|
||||||
|
|
||||||
|
api_version = V1_API_PATH
|
||||||
|
api_content_path = 'artist'
|
||||||
|
api_profile_path = 'qualityprofile'
|
||||||
|
api_status_path = 'system/status'
|
||||||
|
|
||||||
|
content_id_key = 'foreignArtistId'
|
||||||
|
is_lidarr = True
|
||||||
|
|
||||||
|
elif sonarrA_url and sonarrB_url:
|
||||||
|
instanceA_url = sonarrA_url
|
||||||
|
instanceA_key = sonarrA_key
|
||||||
|
instanceA_path = sonarrA_path
|
||||||
|
instanceA_profile = sonarrA_profile
|
||||||
|
instanceA_profile_id = sonarrA_profile_id
|
||||||
|
instanceA_profile_filter = sonarrA_profile_filter
|
||||||
|
instanceA_profile_filter_id = sonarrA_profile_filter_id
|
||||||
|
instanceA_language = sonarrA_language
|
||||||
|
instanceA_language_id = sonarrA_language_id
|
||||||
|
|
||||||
|
instanceB_url = sonarrB_url
|
||||||
|
instanceB_key = sonarrB_key
|
||||||
|
instanceB_path = sonarrB_path
|
||||||
|
instanceB_profile = sonarrB_profile
|
||||||
|
instanceB_profile_id = sonarrB_profile_id
|
||||||
|
instanceB_profile_filter = sonarrB_profile_filter
|
||||||
|
instanceB_profile_filter_id = sonarrB_profile_filter_id
|
||||||
|
instanceB_language = sonarrB_language
|
||||||
|
instanceB_language_id = sonarrB_language_id
|
||||||
|
|
||||||
|
api_version = V3_API_PATH # for sonarr try v3 first
|
||||||
|
api_content_path = 'series'
|
||||||
|
api_profile_path = 'qualityprofile'
|
||||||
|
api_status_path = 'system/status'
|
||||||
|
api_language_path = 'languageprofile'
|
||||||
|
|
||||||
|
content_id_key = 'tvdbId'
|
||||||
|
is_sonarr = True
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
# path generators
|
||||||
|
|
||||||
|
|
||||||
|
def get_path(instance_url, api_path, key, changed_api_version=False):
|
||||||
|
global api_version, api_profile_path
|
||||||
|
|
||||||
|
if changed_api_version:
|
||||||
|
api_version = V3_API_PATH
|
||||||
|
|
||||||
|
# for sonarr - we check v3 first then v2
|
||||||
|
if is_sonarr and changed_api_version:
|
||||||
|
api_version = V2_API_PATH
|
||||||
|
api_profile_path = 'profile'
|
||||||
|
|
||||||
|
logger.debug(DEBUG_LINE)
|
||||||
|
logger.debug({
|
||||||
|
'instance_url': instance_url,
|
||||||
|
'api_path': api_path,
|
||||||
|
'api_version': api_version,
|
||||||
|
'is_sonarr': is_sonarr,
|
||||||
|
'api_profile_path': api_profile_path,
|
||||||
|
'changed_api_version': changed_api_version,
|
||||||
|
})
|
||||||
|
|
||||||
|
url = f"{instance_url}/api/{api_version}{api_path}?apikey={key}"
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
def get_status_path(instance_url, key, changed_api_version):
|
||||||
|
url = get_path(instance_url, api_status_path, key, changed_api_version)
|
||||||
|
logger.debug('get_status_path: {}'.format(url))
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
def get_content_path(instance_url, key):
|
||||||
|
url = get_path(instance_url, api_content_path, key)
|
||||||
|
logger.debug('get_content_path: {}'.format(url))
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
def get_language_path(instance_url, key):
|
||||||
|
url = get_path(instance_url, api_language_path, key)
|
||||||
|
logger.debug('get_language_path: {}'.format(url))
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
def get_profile_path(instance_url, key):
|
||||||
|
url = get_path(instance_url, api_profile_path, key)
|
||||||
|
logger.debug('get_profile_path: {}'.format(url))
|
||||||
|
return url
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
# check for required fields
|
||||||
|
|
||||||
|
|
||||||
|
logger.debug({
|
||||||
|
'instanceA_url': instanceA_url,
|
||||||
|
'instanceA_key': instanceA_key,
|
||||||
|
'instanceA_path': instanceA_path,
|
||||||
|
'instanceB_url': instanceB_url,
|
||||||
|
'instanceB_key': instanceB_key,
|
||||||
|
'instanceB_path': instanceB_path,
|
||||||
|
'api_content_path': api_content_path,
|
||||||
|
'api_profile_path': api_profile_path,
|
||||||
|
'api_language_path': api_language_path,
|
||||||
|
'is_sonarr': is_sonarr,
|
||||||
|
'is_lidarr': is_lidarr,
|
||||||
|
'monitor_new_content': monitor_new_content,
|
||||||
|
'sync_bidirectionally': sync_bidirectionally,
|
||||||
|
'auto_search': auto_search,
|
||||||
|
'api_version': api_version,
|
||||||
|
})
|
||||||
|
|
||||||
|
if not instanceA_url:
|
||||||
|
logger.error('missing URL for instance A')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if not instanceA_key:
|
||||||
|
logger.error('missing API key for instance A')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if not instanceA_url:
|
||||||
|
logger.error('missing URL for instance B')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if not instanceB_key:
|
||||||
|
logger.error('missing API key for instance B')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if not api_content_path:
|
||||||
|
logger.error('missing api_content_path')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if not content_id_key:
|
||||||
|
logger.error('missing content_id_key')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# if two way sync need instance A path and profile
|
||||||
|
if sync_bidirectionally:
|
||||||
|
assert instanceA_path
|
||||||
|
if not instanceB_profile_id and not instanceB_profile:
|
||||||
|
logger.error(
|
||||||
|
'profile_id or profile is required for *arr instance A if sync bidirectionally is enabled')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if not instanceB_profile_id and not instanceB_profile:
|
||||||
|
logger.error('profile_id or profile is required for *arr instance B')
|
||||||
|
sys.exit(0)
|
@ -0,0 +1,389 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import configparser
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
from config import (
|
||||||
|
instanceA_url, instanceA_key, instanceA_path, instanceA_profile,
|
||||||
|
instanceA_profile_id, instanceA_profile_filter, instanceA_profile_filter_id,
|
||||||
|
instanceA_language_id, instanceA_language,
|
||||||
|
|
||||||
|
instanceB_url, instanceB_key, instanceB_path, instanceB_profile,
|
||||||
|
instanceB_profile_id, instanceB_profile_filter, instanceB_profile_filter_id,
|
||||||
|
instanceB_language_id, instanceB_language,
|
||||||
|
|
||||||
|
content_id_key, logger, is_sonarr, is_radarr, is_lidarr,
|
||||||
|
get_status_path, get_content_path, get_profile_path, get_language_path,
|
||||||
|
|
||||||
|
is_in_docker, instance_sync_interval_seconds,
|
||||||
|
sync_bidirectionally, auto_search, monitor_new_content,
|
||||||
|
tested_api_version, api_version, V3_API_PATH,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_new_content_payload(content, instance_path, instance_profile_id, instance_url, instance_language_id=None):
|
||||||
|
global monitor_new_content, auto_search
|
||||||
|
|
||||||
|
images = content.get('images')
|
||||||
|
for image in images:
|
||||||
|
image['url'] = '{0}{1}'.format(instance_url, image.get('url'))
|
||||||
|
|
||||||
|
monitored = content.get('monitored')
|
||||||
|
if monitor_new_content is not None:
|
||||||
|
monitored = True if monitor_new_content else False
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
content_id_key: content.get(content_id_key),
|
||||||
|
'qualityProfileId': instance_profile_id or content.get('qualityProfileId'),
|
||||||
|
'monitored': monitored,
|
||||||
|
'rootFolderPath': instance_path,
|
||||||
|
'images': images,
|
||||||
|
}
|
||||||
|
|
||||||
|
add_options = content.get('addOptions', {})
|
||||||
|
search_missing = True if auto_search else False
|
||||||
|
|
||||||
|
if is_sonarr:
|
||||||
|
payload['title'] = content.get('title')
|
||||||
|
payload['titleSlug'] = content.get('titleSlug')
|
||||||
|
payload['seasons'] = content.get('seasons')
|
||||||
|
payload['year'] = content.get('year')
|
||||||
|
payload['tvRageId'] = content.get('tvRageId')
|
||||||
|
payload['seasonFolder'] = content.get('seasonFolder')
|
||||||
|
payload['languageProfileId'] = instance_language_id if instance_language_id else content.get(
|
||||||
|
'languageProfileId')
|
||||||
|
payload['tags'] = content.get('tags')
|
||||||
|
payload['seriesType'] = content.get('seriesType')
|
||||||
|
payload['useSceneNumbering'] = content.get('useSceneNumbering')
|
||||||
|
payload['addOptions'] = {
|
||||||
|
**add_options,
|
||||||
|
**{'searchForMissingEpisodes': search_missing}
|
||||||
|
}
|
||||||
|
|
||||||
|
elif is_radarr:
|
||||||
|
payload['title'] = content.get('title')
|
||||||
|
payload['year'] = content.get('year')
|
||||||
|
payload['tmdbId'] = content.get('tmdbId')
|
||||||
|
payload['titleSlug'] = content.get('titleSlug')
|
||||||
|
payload['addOptions'] = {
|
||||||
|
**add_options,
|
||||||
|
**{'searchForMovie': search_missing}
|
||||||
|
}
|
||||||
|
|
||||||
|
elif is_lidarr:
|
||||||
|
payload['artistName'] = content.get('artistName')
|
||||||
|
payload['albumFolder'] = content.get('albumFolder')
|
||||||
|
payload['metadataProfileId'] = content.get('metadataProfileId')
|
||||||
|
payload['addOptions'] = {
|
||||||
|
**add_options,
|
||||||
|
**{
|
||||||
|
"monitored": monitored,
|
||||||
|
"searchForMissingAlbums": search_missing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(payload)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def get_profile_from_id(instance_session, instance_url, instance_key, instance_profile, instance_name=''):
|
||||||
|
instance_profile_url = get_profile_path(instance_url, instance_key)
|
||||||
|
profiles_response = instance_session.get(instance_profile_url)
|
||||||
|
if profiles_response.status_code != 200:
|
||||||
|
logger.error(
|
||||||
|
f'Could not get profile id from {instance_profile_url}')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
instance_profiles = None
|
||||||
|
try:
|
||||||
|
instance_profiles = profiles_response.json()
|
||||||
|
except:
|
||||||
|
logger.error(
|
||||||
|
f'Could not decode profile id from {instance_profile_url}')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
profile = next((item for item in instance_profiles if item["name"].lower() == instance_profile.lower()), False)
|
||||||
|
if not profile:
|
||||||
|
logger.error('Could not find profile_id for instance {} profile {}'.format(
|
||||||
|
instance_name, instance_profile))
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
instance_profile_id = profile.get('id')
|
||||||
|
logger.debug(
|
||||||
|
f'found profile_id (instance{instance_name}) "{instance_profile_id}" from profile "{instance_profile}"')
|
||||||
|
|
||||||
|
return instance_profile_id
|
||||||
|
|
||||||
|
|
||||||
|
def get_language_from_id(instance_session, instance_url, instance_key, instance_language, instance_name=''):
|
||||||
|
instance_language_url = get_language_path(instance_url, instance_key)
|
||||||
|
language_response = instance_session.get(instance_language_url)
|
||||||
|
if language_response.status_code != 200:
|
||||||
|
logger.error(
|
||||||
|
f'Could not get language id from (instance{instance_name}) {instance_language_url} - only works on sonarr v3')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
instance_languages = None
|
||||||
|
try:
|
||||||
|
instance_languages = language_response.json()
|
||||||
|
except:
|
||||||
|
logger.error(
|
||||||
|
f'Could not decode language id from {instance_language_url}')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
instance_languages = instance_languages[0]['languages']
|
||||||
|
language = next((item for item in instance_languages
|
||||||
|
if item.get('language', {}).get('name').lower() == instance_language.lower()), False)
|
||||||
|
|
||||||
|
logger.error(language)
|
||||||
|
if not language:
|
||||||
|
logger.error(f'Could not find language_id for instance {instance_name} and language {instance_language}')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
instance_language_id = language.get('language', {}).get('id')
|
||||||
|
logger.debug(f'found id "{instance_language_id}" from language "{instance_language}" for instance {instance_name}')
|
||||||
|
|
||||||
|
if instance_language_id is None:
|
||||||
|
logger.error(f'language_id is None for instance {instance_name} and language {instance_language}')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
return instance_language_id
|
||||||
|
|
||||||
|
|
||||||
|
def sync_servers(instanceA_contents, instanceB_language_id, instanceB_contentIds,
|
||||||
|
instanceB_path, instanceB_profile_id, instanceB_session,
|
||||||
|
instanceB_url, profile_filter_id, instanceB_key):
|
||||||
|
search_ids = []
|
||||||
|
|
||||||
|
# if given instance A profile id then we want to filter out content without that id
|
||||||
|
if profile_filter_id:
|
||||||
|
logging.info(f'only filtering content with profile_filter_id {profile_filter_id}')
|
||||||
|
|
||||||
|
# for each content id in instance A, check if it needs to be synced to instance B
|
||||||
|
for content in instanceA_contents:
|
||||||
|
if content[content_id_key] not in instanceB_contentIds:
|
||||||
|
title = content.get('title') or content.get('artistName')
|
||||||
|
|
||||||
|
# if given this, we want to filter from instance by profile id
|
||||||
|
if profile_filter_id:
|
||||||
|
content_profile_id = content.get('qualityProfileId')
|
||||||
|
if profile_filter_id != content_profile_id:
|
||||||
|
logging.debug(f'Skipping content {title} - mismatched profile_id {content_profile_id} with filter_id {profile_filter_id}')
|
||||||
|
continue
|
||||||
|
|
||||||
|
logging.info(f'syncing content title "{title}"')
|
||||||
|
|
||||||
|
# get the POST payload and sync content to instance B
|
||||||
|
payload = get_new_content_payload(
|
||||||
|
content=content,
|
||||||
|
instance_path=instanceB_path,
|
||||||
|
instance_profile_id=instanceB_profile_id,
|
||||||
|
instance_url=instanceB_url,
|
||||||
|
instance_language_id=instanceB_language_id,
|
||||||
|
)
|
||||||
|
instanceB_content_url = get_content_path(instanceB_url, instanceB_key)
|
||||||
|
sync_response = instanceB_session.post(instanceB_content_url, data=json.dumps(payload))
|
||||||
|
|
||||||
|
# check response and save content id for searching later on if success
|
||||||
|
if sync_response.status_code != 201 and sync_response.status_code != 200:
|
||||||
|
logger.error(f'server sync error for {title} - response: {sync_response.text}')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
search_ids.append(int(sync_response.json()['id']))
|
||||||
|
except:
|
||||||
|
logger.error(f'Could not decode sync response from {instanceB_content_url}')
|
||||||
|
logging.info('content title "{0}" synced successfully'.format(title))
|
||||||
|
|
||||||
|
logging.info(f'{len(search_ids)} contents synced successfully')
|
||||||
|
|
||||||
|
|
||||||
|
def get_instance_contents(instance_url, instance_key, instance_session, instance_name=''):
|
||||||
|
instance_contentIds = []
|
||||||
|
|
||||||
|
instance_content_url = get_content_path(instance_url, instance_key)
|
||||||
|
instance_contents = instance_session.get(instance_content_url)
|
||||||
|
|
||||||
|
if instance_contents.status_code != 200:
|
||||||
|
logger.error('instance{} server error - response {}'.format(instance_name, instance_contents.status_code))
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
instance_contents = instance_contents.json()
|
||||||
|
except:
|
||||||
|
logger.error(f'Could not decode contents from {instance_content_url}')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
for content_to_sync in instance_contents:
|
||||||
|
instance_contentIds.append(content_to_sync[content_id_key])
|
||||||
|
|
||||||
|
logger.debug('{} contents in instance{}'.format(len(instance_contentIds), instance_name))
|
||||||
|
return instance_contents, instance_contentIds
|
||||||
|
|
||||||
|
|
||||||
|
def check_status(instance_session, instance_url, instance_key, instance_name='', changed_api_version=False):
|
||||||
|
global api_version
|
||||||
|
|
||||||
|
instance_status_url = get_status_path(
|
||||||
|
instance_url, instance_key, changed_api_version)
|
||||||
|
error_message = f'Could not connect to instance{instance_name}: {instance_status_url}'
|
||||||
|
status_response = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
status_response = instance_session.get(instance_status_url)
|
||||||
|
|
||||||
|
# only test again if not lidarr and we haven't tested v3 already
|
||||||
|
if status_response.status_code != 200 and not changed_api_version and not is_lidarr:
|
||||||
|
logger.debug(f'check api_version again')
|
||||||
|
status_response = check_status(instance_session, instance_url, instance_key, instance_name, True)
|
||||||
|
elif status_response.status_code != 200:
|
||||||
|
logger.error(error_message)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
except:
|
||||||
|
if not changed_api_version and not is_lidarr:
|
||||||
|
logger.debug(f'check api_version again exception')
|
||||||
|
status_response = check_status(
|
||||||
|
instance_session, instance_url, instance_key, instance_name, True)
|
||||||
|
|
||||||
|
if status_response is None:
|
||||||
|
logger.error(error_message)
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
status_response = status_response.json()
|
||||||
|
except Exception as error:
|
||||||
|
if not isinstance(status_response, dict):
|
||||||
|
logger.error(
|
||||||
|
f"Could not retrieve status for {instance_status_url}: {status_response} - {error}")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if(status_response.get('error')):
|
||||||
|
logger.error(f"{instance_status_url} error {status_response.get('error')}")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
logger.debug(f"{instance_status_url} version {status_response.get('version')}")
|
||||||
|
|
||||||
|
return status_response
|
||||||
|
|
||||||
|
|
||||||
|
def sync_content():
|
||||||
|
global instanceA_profile_id, instanceA_profile, instanceB_profile_id, instanceB_profile, instanceA_profile_filter, instanceA_profile_filter_id, instanceB_profile_filter, instanceB_profile_filter_id, tested_api_version, instanceA_language_id, instanceA_language, instanceB_language_id, instanceB_language
|
||||||
|
|
||||||
|
# get sessions
|
||||||
|
instanceA_session = requests.Session()
|
||||||
|
instanceA_session.trust_env = False
|
||||||
|
instanceB_session = requests.Session()
|
||||||
|
instanceB_session.trust_env = False
|
||||||
|
|
||||||
|
if not tested_api_version:
|
||||||
|
check_status(instanceA_session, instanceA_url, instanceA_key, instance_name='A')
|
||||||
|
check_status(instanceB_session, instanceB_url, instanceB_key, instance_name='B')
|
||||||
|
tested_api_version = True
|
||||||
|
|
||||||
|
# if given a profile instead of a profile id then try to find the profile id
|
||||||
|
if not instanceA_profile_id and instanceA_profile:
|
||||||
|
instanceA_profile_id = get_profile_from_id(
|
||||||
|
instanceA_session, instanceA_url, instanceA_key, instanceA_profile, 'A')
|
||||||
|
if not instanceB_profile_id and instanceB_profile:
|
||||||
|
instanceB_profile_id = get_profile_from_id(
|
||||||
|
instanceB_session, instanceB_url, instanceB_key, instanceB_profile, 'B')
|
||||||
|
logger.debug({
|
||||||
|
'instanceA_profile_id': instanceA_profile_id,
|
||||||
|
'instanceA_profile': instanceA_profile,
|
||||||
|
'instanceB_profile_id': instanceB_profile_id,
|
||||||
|
'instanceB_profile': instanceB_profile,
|
||||||
|
})
|
||||||
|
|
||||||
|
# do the same for profile id filters if they exist
|
||||||
|
if not instanceA_profile_filter_id and instanceA_profile_filter:
|
||||||
|
instanceA_profile_filter_id = get_profile_from_id(
|
||||||
|
instanceA_session, instanceA_url, instanceA_key, instanceA_profile_filter, 'A')
|
||||||
|
if not instanceB_profile_filter_id and instanceB_profile_filter:
|
||||||
|
instanceB_profile_filter_id = get_profile_from_id(
|
||||||
|
instanceB_session, instanceB_url, instanceB_key, instanceB_profile_filter, 'B')
|
||||||
|
logger.debug({
|
||||||
|
'instanceAprofile_filter_id': instanceA_profile_filter_id,
|
||||||
|
'instanceAprofile_filter': instanceA_profile_filter,
|
||||||
|
'instanceBprofile_filter_id': instanceB_profile_filter_id,
|
||||||
|
'instanceBprofile_filter': instanceB_profile_filter,
|
||||||
|
})
|
||||||
|
|
||||||
|
# if given language instead of language id then try to find the lanaguage id
|
||||||
|
# only for sonarr v3
|
||||||
|
if is_sonarr:
|
||||||
|
if not instanceA_language_id and instanceA_language:
|
||||||
|
instanceA_language_id = get_language_from_id(
|
||||||
|
instance_session=instanceA_session,
|
||||||
|
instance_url=instanceA_url,
|
||||||
|
instance_key=instanceA_key,
|
||||||
|
instance_language=instanceA_language,
|
||||||
|
instance_name='A'
|
||||||
|
)
|
||||||
|
|
||||||
|
if not instanceB_language_id and instanceB_language:
|
||||||
|
instanceB_language_id = get_language_from_id(
|
||||||
|
instance_session=instanceB_session,
|
||||||
|
instance_url=instanceB_url,
|
||||||
|
instance_key=instanceB_key,
|
||||||
|
instance_language=instanceB_language,
|
||||||
|
instance_name='B'
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug({
|
||||||
|
'instanceA_language_id': instanceA_language_id,
|
||||||
|
'instanceA_language': instanceA_language,
|
||||||
|
'instanceB_language_id': instanceB_language_id,
|
||||||
|
'instanceB_language': instanceB_language,
|
||||||
|
'is_sonarr': is_sonarr,
|
||||||
|
'api_version': api_version,
|
||||||
|
})
|
||||||
|
|
||||||
|
# get contents to compare
|
||||||
|
instanceA_contents, instanceA_contentIds = get_instance_contents(instanceA_url, instanceA_key, instanceA_session, instance_name='A')
|
||||||
|
instanceB_contents, instanceB_contentIds = get_instance_contents(instanceB_url, instanceB_key, instanceB_session, instance_name='B')
|
||||||
|
|
||||||
|
logger.info('syncing content from instance A to instance B')
|
||||||
|
sync_servers(
|
||||||
|
instanceA_contents=instanceA_contents,
|
||||||
|
instanceB_contentIds=instanceB_contentIds,
|
||||||
|
instanceB_language_id=instanceB_language_id,
|
||||||
|
instanceB_path=instanceB_path,
|
||||||
|
instanceB_profile_id=instanceB_profile_id,
|
||||||
|
instanceB_session=instanceB_session,
|
||||||
|
instanceB_url=instanceB_url,
|
||||||
|
profile_filter_id=instanceA_profile_filter_id,
|
||||||
|
instanceB_key=instanceB_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
# if given bidirectional flag then sync from instance B to instance A
|
||||||
|
if sync_bidirectionally:
|
||||||
|
logger.info('syncing content from instance B to instance A')
|
||||||
|
|
||||||
|
sync_servers(
|
||||||
|
instanceA_contents=instanceB_contents,
|
||||||
|
instanceB_contentIds=instanceA_contentIds,
|
||||||
|
instanceB_language_id=instanceA_language_id,
|
||||||
|
instanceB_path=instanceA_path,
|
||||||
|
instanceB_profile_id=instanceA_profile_id,
|
||||||
|
instanceB_session=instanceA_session,
|
||||||
|
instanceB_url=instanceA_url,
|
||||||
|
profile_filter_id=instanceB_profile_filter_id,
|
||||||
|
instanceB_key=instanceA_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
if is_in_docker:
|
||||||
|
logger.info('syncing every {} seconds'.format(instance_sync_interval_seconds))
|
||||||
|
|
||||||
|
sync_content()
|
||||||
|
|
||||||
|
if is_in_docker:
|
||||||
|
while True:
|
||||||
|
time.sleep(instance_sync_interval_seconds)
|
||||||
|
sync_content()
|
@ -0,0 +1,2 @@
|
|||||||
|
requests==2.20.0
|
||||||
|
configparser==3.5.0
|
Loading…
Reference in new issue