├── Config.default ├── README.md └── RadarrSync.py /Config.default: -------------------------------------------------------------------------------- 1 | [General] 2 | # Time to wait between adding new movies to a server. This will help reduce the load of the Sync server. 0 to disable. (seconds) 3 | wait_between_add = 5 4 | 5 | # Full path to log file 6 | log_path = ./Output.txt 7 | 8 | # DEBUG, INFO, VERBOSE | Logging level. 9 | log_level = DEBUG 10 | 11 | [RadarrMaster] 12 | url = http://127.0.0.1:7878 13 | key = XXXXXXXXXXXXXXXXXX 14 | 15 | 16 | [SyncServers] 17 | # Ensure the servers start with 'Radarr_' 18 | [Radarr_4k] 19 | url = http://127.0.0.1:7879 20 | key = XXXXXXXXXXXXXXXXXX 21 | 22 | rootFolders = /Movies 23 | # If this path exists 24 | current_path = /Movies/ 25 | # Replace with this path 26 | new_path = /Movies4k/ 27 | 28 | # This is the profile ID the movie will be added to. 29 | profileId = 5 30 | 31 | # This is the profile ID the movie must have on the Master server. 32 | profileIdMatch = 4 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UNSUPPORTED USE AT YOUR OWN RISK !!! 2 | Should probably try the original https://github.com/Sperryfreak01/RadarrSync 3 | 4 | # Credits 5 | Thanks to https://github.com/Sperryfreak01/RadarrSync for the initial inspiration that lead to https://github.com/hjone72/RadarrSync MultiServer branch. 6 | 7 | # RadarrSync 8 | Syncs two Radarr servers through web API. 9 | 10 | ### Why 11 | Many Plex servers choke if you try to transcode 4K files. To address this a common approach is to keep a 4k and a 1080/720 version in separate libraries. 12 | 13 | Radarr does not support saving files to different folder roots for different quality profiles. To save 4K files to a separate library in plex you must run two Radarr servers. This script looks for movies with a specific quality setting on one server and creates the movies on a second server. 14 | 15 | 16 | ### Configuration 17 | 1. Edit the Config.txt file and enter your servers URLs and API keys for each server. 18 | 19 | Example Config.txt: 20 | ```ini 21 | [General] 22 | # Time to wait between adding new movies to a server. This will help reduce the load of the Sync server. 0 to disable. (seconds) 23 | wait_between_add = 5 24 | 25 | # Full path to log file 26 | log_path = ./Output.txt 27 | 28 | # DEBUG, INFO, VERBOSE | Logging level. 29 | log_level = DEBUG 30 | 31 | [RadarrMaster] 32 | url = http://127.0.0.1:7878 33 | key = XXXX-XXXX-XXXX-XXXX-XXXX 34 | 35 | [SyncServers] 36 | # Ensure the servers start with 'Radarr_' 37 | [Radarr_4k] 38 | url = http://127.0.0.1:7879 39 | key = XXXX-XXXX-XXXX-XXXX-XXXX 40 | 41 | # Only sync movies that are in these root folders. ';' (semicolon) separated list. Remove line to disable. 42 | rootFolders = /Movies 43 | 44 | # If this path exists 45 | current_path = /Movies/ 46 | # Replace with this path 47 | new_path = /Movies4k/ 48 | 49 | # This is the profile ID the movie will be added to. 50 | profileId = 5 51 | 52 | # This is the profile ID the movie must have on the Master server. 53 | profileIdMatch = 4 54 | ``` 55 | 2. Find the profileIdMatch on the Master server. Usually just count starting from Any: #1 SD: #2 etc.... IE: if you use the default HD-1080p proflie that would be #4. 56 | 3. Change profileId configuration to what you want the profile to be on the SyncServer. In most cases you will want to use #5. 57 | 58 | 59 | #### How to Run 60 | Recomended to run using cron every 15 minutes or an interval of your preference. 61 | ```bash 62 | python3 RadarrSync.py 63 | ``` 64 | To test without running use: 65 | ```bash 66 | python3 RadarrSync.py --debug --whatif 67 | ``` 68 | #### Requirements 69 | -- Python 3.4 or greater 70 | -- 2 or more Radarr servers 71 | -------------------------------------------------------------------------------- /RadarrSync.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import json 4 | import sys 5 | import requests 6 | import configparser 7 | import argparse 8 | import shutil 9 | import time 10 | 11 | parser = argparse.ArgumentParser(description='RadarrSync. Sync two or more Radarr servers. https://github.com/Sperryfreak01/RadarrSync') 12 | parser.add_argument('--config', action="store", type=str, help='Location of config file.') 13 | parser.add_argument('--debug', help='Enable debug logging.', action="store_true") 14 | parser.add_argument('--whatif', help="Read-Only. What would happen if I ran this. No posts are sent. Should be used with --debug", action="store_true") 15 | args = parser.parse_args() 16 | 17 | def ConfigSectionMap(section): 18 | dict1 = {} 19 | options = Config.options(section) 20 | for option in options: 21 | try: 22 | dict1[option] = Config.get(section, option) 23 | if dict1[option] == -1: 24 | logger.debug("skip: %s" % option) 25 | except: 26 | print("exception on %s!" % option) 27 | dict1[option] = None 28 | return dict1 29 | 30 | Config = configparser.ConfigParser() 31 | settingsFilename = os.path.join(os.getcwd(), 'Config.txt') 32 | if args.config: 33 | settingsFilename = args.config 34 | elif not os.path.isfile(settingsFilename): 35 | print("Creating default config. Please edit and run again.") 36 | shutil.copyfile(os.path.join(os.getcwd(), 'Config.default'), settingsFilename) 37 | sys.exit(0) 38 | Config.read(settingsFilename) 39 | 40 | print(ConfigSectionMap('Radarr_4k')['rootfolders'].split(';')) 41 | 42 | ######################################################################################################################## 43 | logger = logging.getLogger() 44 | if ConfigSectionMap("General")['log_level'] == 'DEBUG': 45 | logger.setLevel(logging.DEBUG) 46 | elif ConfigSectionMap("General")['log_level'] == 'VERBOSE': 47 | logger.setLevel(logging.VERBOSE) 48 | else: 49 | logger.setLevel(logging.INFO) 50 | if args.debug: 51 | logger.setLevel(logging.DEBUG) 52 | 53 | logFormatter = logging.Formatter("%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s") 54 | 55 | fileHandler = logging.FileHandler(ConfigSectionMap('General')['log_path'],'w','utf-8') 56 | fileHandler.setFormatter(logFormatter) 57 | logger.addHandler(fileHandler) 58 | 59 | consoleHandler = logging.StreamHandler(sys.stdout) 60 | consoleHandler.setFormatter(logFormatter) 61 | logger.addHandler(consoleHandler) 62 | ######################################################################################################################## 63 | 64 | session = requests.Session() 65 | session.trust_env = False 66 | 67 | radarr_url = ConfigSectionMap("RadarrMaster")['url'] 68 | radarr_key = ConfigSectionMap("RadarrMaster")['key'] 69 | radarrMovies = session.get('{0}/api/movie?apikey={1}'.format(radarr_url, radarr_key)) 70 | if radarrMovies.status_code != 200: 71 | logger.error('Master Radarr server error - response {}'.format(radarrMovies.status_code)) 72 | sys.exit(0) 73 | 74 | servers = {} 75 | for section in Config.sections(): 76 | section = str(section) 77 | if "Radarr_" in section: 78 | server = (str.split(section,'Radarr_'))[1] 79 | servers[server] = ConfigSectionMap(section) 80 | movies = session.get('{0}/api/movie?apikey={1}'.format(servers[server]['url'], servers[server]['key'])) 81 | if movies.status_code != 200: 82 | logger.error('{0} Radarr server error - response {1}'.format(server, movies.status_code)) 83 | sys.exit(0) 84 | else: 85 | servers[server]['movies'] = [] 86 | servers[server]['newMovies'] = 0 87 | servers[server]['searchid'] = [] 88 | for movie in movies.json(): 89 | servers[server]['movies'].append(movie['tmdbId']) 90 | 91 | for movie in radarrMovies.json(): 92 | for name, server in servers.items(): 93 | if movie['profileId'] == int(server['profileidmatch']): 94 | if movie['tmdbId'] not in server['movies']: 95 | if 'rootfolders' in server: 96 | allowedFolders = server['rootfolders'].split(';') 97 | for folder in allowedFolders: 98 | if not folder in movie['path']: 99 | continue 100 | if 'current_path' in server: 101 | path = str(movie['path']).replace(server['current_path'], server['new_path']) 102 | logging.debug('Updating movie path from: {0} to {1}'.format(movie['path'], path)) 103 | else: 104 | path = movie['path'] 105 | logging.debug('server: {0}'.format(name)) 106 | logging.debug('title: {0}'.format(movie['title'])) 107 | logging.debug('qualityProfileId: {0}'.format(server['profileid'])) 108 | logging.debug('titleSlug: {0}'.format(movie['titleSlug'])) 109 | images = movie['images'] 110 | for image in images: 111 | image['url'] = '{0}{1}'.format(radarr_url, image['url']) 112 | logging.debug(image['url']) 113 | logging.debug('tmdbId: {0}'.format(movie['tmdbId'])) 114 | logging.debug('path: {0}'.format(path)) 115 | logging.debug('monitored: {0}'.format(movie['monitored'])) 116 | 117 | payload = {'title': movie['title'], 118 | 'qualityProfileId': server['profileid'], 119 | 'titleSlug': movie['titleSlug'], 120 | 'tmdbId': movie['tmdbId'], 121 | 'path': path, 122 | 'monitored': movie['monitored'], 123 | 'images': images, 124 | 'profileId': server['profileid'], 125 | 'minimumAvailability': 'released' 126 | } 127 | 128 | logging.debug('payload: {0}'.format(payload)) 129 | server['newMovies'] += 1 130 | if args.whatif: 131 | logging.debug('WhatIf: Not actually adding movie to Radarr {0}.'.format(name)) 132 | else: 133 | if server['newMovies'] > 0: 134 | logging.debug('Sleeping for: {0} seconds.'.format(ConfigSectionMap('General')['wait_between_add'])) 135 | time.sleep(int(ConfigSectionMap('General')['wait_between_add'])) 136 | r = session.post('{0}/api/movie?apikey={1}'.format(server['url'], server['key']), data=json.dumps(payload)) 137 | if (r.status_code == 200 or r.status_code == 201): 138 | server['searchid'].append(int(r.json()['id'])) 139 | logger.info('adding {0} to Radarr {1} server'.format(movie['title'], name)) 140 | else: 141 | logger.error('adding {0} to Radarr {1} server failed. Response {2}'.format(movie['title'],name,r.text)) 142 | else: 143 | logging.debug('{0} already in {1} library'.format(movie['title'], name)) 144 | 145 | for name, server in servers.items(): 146 | if len(server['searchid']): 147 | payload = {'name' : 'MoviesSearch', 'movieIds' : server['searchid']} 148 | session.post('{0}/api/command?apikey={1}'.format(server['url'], server['key']),data=json.dumps(payload)) 149 | --------------------------------------------------------------------------------