From 2c387878529804653d3d724a1d94e8881cf70137 Mon Sep 17 00:00:00 2001 From: Junkbite Date: Mon, 7 Jun 2021 14:44:02 -0400 Subject: [PATCH] add sync monitoring --- README.md | 18 ++++++++------ config.py | 73 +++++++++++++++++++------------------------------------ index.py | 53 ++++++++++++++++++++++++++++++---------- 3 files changed, 75 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index c5da110..b79e70b 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ Syncs two Radarr/Sonarr/Lidarr servers through the web API. Useful for syncing a * Can set interval for syncing * Support two way sync (one way by default) * Skip content with missing files -* Set language profiles (Sonarr v3 only) +* Set language profiles (Sonarr) * Filter syncing by content file quality (Radarr only) -* Filter syncing by tags (Sonarr/Radarr v3 only) +* Filter syncing by tags (Sonarr/Radarr) * Allow for a test run using `test_run` flag (does everything but actually sync) ## Configuration @@ -68,8 +68,8 @@ Syncs two Radarr/Sonarr/Lidarr servers through the web API. Useful for syncing a key = XXXXX profile_filter = 1080p # add a filter to only sync contents belonging to this profile (can set by profile_filter_id as well) quality_match = HD- # (Radarr only) regex match to only sync content that matches the set quality (ie if set to 1080p then only movies with matching downloaded quality of 1080p will be synced) - tag_filter = Horror # (Sonarr/Radarr v3 only) sync movies by tag name (seperate multiple tags by comma (no spaces) ie horror,comedy,action) - tag_filter_id = 2 # (Sonarr/Radarr v3 only) sync movies by tag id (seperate multiple tags by comma (no spaces) ie 2,3,4) + tag_filter = Horror # (Sonarr/Radarr) sync movies by tag name (seperate multiple tags by comma (no spaces) ie horror,comedy,action) + tag_filter_id = 2 # (Sonarr/Radarr) sync movies by tag id (seperate multiple tags by comma (no spaces) ie 2,3,4) blacklist = movie-name-12,movie-name-43,432534,8e38819d-71be-9e7d-b41d-f1df91b01d3f # comma seperated list of content slugs OR IDs you want to never sync from A to B (no spaces) # the slug is the part of the URL after "/movies/" (for Radarr), "/series/" (for Sonarr), or "/artist/" (for Lidarr) @@ -77,15 +77,16 @@ Syncs two Radarr/Sonarr/Lidarr servers through the web API. Useful for syncing a 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) + language = Vietnamese # can set language for new content added (Sonarr) (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 + sync_bidirectionally = 1 # sync from instance A to B **AND** instance B to A (default 0) auto_search = 0 # search is automatically started on new content - disable by setting to 0 (default 1) skip_missing = 1 # content with missing files are skipped on sync - disable by setting to 0 (default 1) monitor_new_content = 0 # set to 0 to never monitor new content synced or to 1 to always monitor new content synced (default 1) - test_run = 1 # enable test mode - will run through sync program but will not actually sync content + test_run = 1 # enable test mode - will run through sync program but will not actually sync content (default 0) + sync_monitor = 1 # if set to 1 will sync if the content is monitored or not to instance B (default 0) ``` **Note** If `sync_bidirectionally` is set to `1`, then instance A will require either `profile_id` or `profile` AND `path` as well @@ -175,7 +176,7 @@ docker run -it --rm --name syncarr -e RADARR_A_URL=https://4k.example.com:443 -e * 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) +* Language for new content (Sonarr) 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` (default 0) * Set disable auto searching on new content with `SYNCARR_AUTO_SEARCH=0` (default 1) * Set if you want to NOT monitor new content with `SYNCARR_MONITOR_NEW_CONTENT=0` (default 1) @@ -183,6 +184,7 @@ To filter by profile in docker use `*ARR_A_PROFILE_FILTER` or `*ARR_A_PROFILE_FI * Filter by tag names or ids with `*ARR_A_TAG_FILTER` / `*ARR_B_TAG_FILTER` or `*ARR_A_TAG_FILTER_ID` / `*ARR_B_TAG_FILTER_ID` * Enable test mode with `SYNCARR_TEST_RUN` * add blacklist with `*ARR_A_BLACKLIST` and `**ARR_B_BLACKLIST` +* sync monitor settings with `SYNCARR_SYNC_MONITOR` --- diff --git a/config.py b/config.py index fe615e1..f1c8167 100644 --- a/config.py +++ b/config.py @@ -150,55 +150,26 @@ lidarrB_language_id = get_config_value('LIDARR_B_LANGUAGE_ID', 'language_id', 'l lidarrB_quality_match = get_config_value('LIDARR_B_QUALITY_MATCH', 'quality_match', 'lidarrB') lidarrB_blacklist = get_config_value('LIDARR_B_BLACKLIST', 'blacklist', 'lidarrB') -# set to search if config not set -sync_bidirectionally = get_config_value('SYNCARR_BIDIRECTIONAL_SYNC', 'bidirectional', 'general') -if sync_bidirectionally is not None: - try: - sync_bidirectionally = int(sync_bidirectionally) - except ValueError: - sync_bidirectionally = 0 -else: - sync_bidirectionally = 0 - -# set to search if config not set -auto_search = get_config_value('SYNCARR_AUTO_SEARCH', 'auto_search', 'general') -if auto_search is not None: - try: - auto_search = int(auto_search) - except ValueError: - auto_search = 0 -else: - auto_search = 1 - -# set to skip missing if config not set -skip_missing = get_config_value('SYNCARR_SKIP_MISSING', 'skip_missing', 'general') -if skip_missing is not None: - try: - skip_missing = int(skip_missing) - except ValueError: - skip_missing = 0 -else: - skip_missing = 1 -# set to monitor if config not set -monitor_new_content = get_config_value('SYNCARR_MONITOR_NEW_CONTENT', 'monitor_new_content', 'general') -if monitor_new_content is not None: - try: - monitor_new_content = int(monitor_new_content) - except ValueError: - monitor_new_content = 1 -else: - monitor_new_content = 1 - -# enable test mode -is_test_run = get_config_value('SYNCARR_TEST_RUN', 'test_run', 'general') -if is_test_run is not None: - try: - is_test_run = int(is_test_run) - except ValueError: - is_test_run = 0 -else: - is_test_run = 0 +def set_general_option(env_name, config_name, default_value): + """gets a general config value""" + config_value = get_config_value(env_name, config_name, 'general') + if config_value is not None: + try: + config_value = int(config_value) + except ValueError: + config_value = default_value + else: + config_value = default_value + return config_value + +# get general options +sync_bidirectionally = set_general_option('SYNCARR_BIDIRECTIONAL_SYNC', 'bidirectional', default_value=0) +auto_search = set_general_option('SYNCARR_AUTO_SEARCH', 'auto_search', default_value=1) +skip_missing = set_general_option('SYNCARR_SKIP_MISSING', 'skip_missing', default_value=1) +monitor_new_content = set_general_option('SYNCARR_MONITOR_NEW_CONTENT', 'monitor_new_content', default_value=1) +sync_monitor = set_general_option('SYNCARR_SYNC_MONITOR', 'sync_monitor', default_value=0) +is_test_run = set_general_option('SYNCARR_TEST_RUN', 'test_run', default_value=0) ######################################################################################################################## # setup logger @@ -437,6 +408,11 @@ def get_content_path(instance_url, key): logger.debug('get_content_path: {}'.format(url)) return url +def get_content_put_path(instance_url, key, content_id): + url = get_path(instance_url, f'{api_content_path}/{content_id}', key) + logger.debug('get_content_put_path: {}'.format(url)) + return url + def get_language_path(instance_url, key): url = get_path(instance_url, api_language_path, key) @@ -501,6 +477,7 @@ logger.debug({ 'auto_search': auto_search, 'skip_missing': skip_missing, 'api_version': api_version, + 'sync_monitor': sync_monitor, }) if not instanceA_url: diff --git a/index.py b/index.py index b31950e..f12c1e8 100644 --- a/index.py +++ b/index.py @@ -22,15 +22,16 @@ from config import ( instanceB_tag_filter_id, instanceB_tag_filter, instanceB_blacklist, content_id_key, logger, is_sonarr, is_radarr, is_lidarr, - get_status_path, get_content_path, get_profile_path, get_language_path, get_tag_path, + get_status_path, get_content_path, get_profile_path, get_language_path, get_tag_path, get_content_put_path, is_in_docker, instance_sync_interval_seconds, sync_bidirectionally, auto_search, skip_missing, monitor_new_content, - api_version, is_test_run, + api_version, is_test_run, sync_monitor ) -def get_new_content_payload(content, instance_path, instance_profile_id, instance_url, instance_language_id=None): +def get_content_details(content, instance_path, instance_profile_id, instance_url, instance_language_id=None): + """gets details of a content item""" global monitor_new_content, auto_search images = content.get('images') @@ -193,8 +194,8 @@ def get_language_from_id(instance_session, instance_url, instance_key, instance_ def sync_servers(instanceA_contents, instanceB_language_id, instanceB_contentIds, instanceB_path, instanceB_profile_id, instanceA_profile_filter_id, instanceB_session, instanceB_url, instanceB_key, instanceA_quality_match, - instanceA_tag_filter_id, instanceA_blacklist): - global is_radarr, is_sonarr, is_test_run + instanceA_tag_filter_id, instanceA_blacklist, instanceB_contents): + global is_radarr, is_sonarr, is_test_run, sync_monitor search_ids = [] # if given instance A profile id then we want to filter out content without that id @@ -203,7 +204,9 @@ def sync_servers(instanceA_contents, instanceB_language_id, instanceB_contentIds # 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: + content_not_synced = content[content_id_key] not in instanceB_contentIds + # only skip alrerady synced items if we arent syncing monitoring as well + if content_not_synced or sync_monitor: title = content.get('title') or content.get('artistName') instance_path = instanceB_path or dirname(content.get('path')) @@ -247,11 +250,10 @@ def sync_servers(instanceA_contents, instanceB_language_id, instanceB_contentIds logging.debug(f'Skipping content {title} - blacklist ID: {content_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, + # generate content from instance A to sync into instance B + formatted_content = get_content_details( + content=dict(content), instance_path=instance_path, instance_profile_id=instanceB_profile_id, instance_url=instanceB_url, @@ -261,9 +263,10 @@ def sync_servers(instanceA_contents, instanceB_language_id, instanceB_contentIds if is_test_run: logging.info('content title "{0}" synced successfully (test only)'.format(title)) - else: - sync_response = instanceB_session.post(instanceB_content_url, data=json.dumps(payload)) - + elif content_not_synced: + # sync content if not synced + logging.info(f'syncing content title "{title}"') + sync_response = instanceB_session.post(instanceB_content_url, data=json.dumps(formatted_content)) # 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}') @@ -274,6 +277,28 @@ def sync_servers(instanceA_contents, instanceB_language_id, instanceB_contentIds logger.error(f'Could not decode sync response from {instanceB_content_url}') logging.info('content title "{0}" synced successfully'.format(title)) + elif sync_monitor: + # else if is already synced and we want to sync monitoring then sync that now + + # find matching content from instance B to check monitored status + matching_content_instanceB = list(filter(lambda content_instanceB: content_instanceB['titleSlug'] == content.get('titleSlug'), instanceB_contents)) + if(len(matching_content_instanceB) == 1): + matching_content_instanceB = matching_content_instanceB[0] + # if we found a content match from instance B, then check monitored status - if different then sync from A to B + if matching_content_instanceB['monitored'] != content['monitored']: + matching_content_instanceB['monitored'] = content['monitored'] + instanceB_content_url = get_content_put_path(instanceB_url, instanceB_key, matching_content_instanceB.get('id')) + sync_response = instanceB_session.put(instanceB_content_url, data=json.dumps(matching_content_instanceB)) + # check response and save content id for searching later on if success + if sync_response.status_code != 202: + logger.error(f'server monitoring 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}" monitoring synced successfully'.format(title)) + logging.info(f'{len(search_ids)} contents synced successfully') @@ -417,6 +442,7 @@ def sync_content(): logger.info('syncing content from instance A to instance B') sync_servers( instanceA_contents=instanceA_contents, + instanceB_contents=instanceB_contents, instanceB_contentIds=instanceB_contentIds, instanceB_language_id=instanceB_language_id, instanceB_path=instanceB_path, @@ -436,6 +462,7 @@ def sync_content(): sync_servers( instanceA_contents=instanceB_contents, + instanceB_contents=instanceA_contents, instanceB_contentIds=instanceA_contentIds, instanceB_language_id=instanceA_language_id, instanceB_path=instanceA_path,