Fix username differences in watch list. Add python version check. More error handling.

This commit is contained in:
Luigi311
2022-06-19 02:56:50 -06:00
parent 21fe4875eb
commit 74b5ea7b5e
5 changed files with 795 additions and 728 deletions

401
main.py
View File

@@ -1,397 +1,10 @@
import copy, os, traceback, json import sys
from dotenv import load_dotenv
from time import sleep
if __name__ == '__main__':
# Check python version 3.6 or higher
if not (3, 6) <= tuple(map(int, sys.version_info[:2])):
print("This script requires Python 3.6 or higher")
sys.exit(1)
from src.functions import logger, str_to_bool, search_mapping, generate_library_guids_dict, future_thread_executor from src.main import main
from src.plex import Plex
from src.jellyfin import Jellyfin
load_dotenv(override=True)
def cleanup_watched(watched_list_1, watched_list_2, user_mapping=None, library_mapping=None):
modified_watched_list_1 = copy.deepcopy(watched_list_1)
# remove entries from plex_watched that are in jellyfin_watched
for user_1 in watched_list_1:
user_other = None
if user_mapping:
user_other = search_mapping(user_mapping, user_1)
if user_1 in modified_watched_list_1:
if user_1 in watched_list_2:
user_2 = user_1
elif user_other in watched_list_2:
user_2 = user_other
else:
logger(f"User {user_1} and {user_other} not found in watched list 2", 1)
continue
for library_1 in watched_list_1[user_1]:
library_other = None
if library_mapping:
library_other = search_mapping(library_mapping, library_1)
if library_1 in modified_watched_list_1[user_1]:
if library_1 in watched_list_2[user_2]:
library_2 = library_1
elif library_other in watched_list_2[user_2]:
library_2 = library_other
else:
logger(f"library {library_1} and {library_other} not found in watched list 2", 1)
continue
# Movies
if isinstance(watched_list_1[user_1][library_1], list):
for item in watched_list_1[user_1][library_1]:
for watch_list_1_key, watch_list_1_value in item.items():
for watch_list_2_item in watched_list_2[user_2][library_2]:
for watch_list_2_item_key, watch_list_2_item_value in watch_list_2_item.items():
if watch_list_1_key == watch_list_2_item_key and watch_list_1_value == watch_list_2_item_value:
if item in modified_watched_list_1[user_1][library_1]:
logger(f"Removing {item} from {library_1}", 3)
modified_watched_list_1[user_1][library_1].remove(item)
# TV Shows
elif isinstance(watched_list_1[user_1][library_1], dict):
# Generate full list of provider ids for episodes in watch_list_2 to easily compare if they exist in watch_list_1
_, episode_watched_list_2_keys_dict, _ = generate_library_guids_dict(watched_list_2[user_2][library_2], 1)
for show_key_1 in watched_list_1[user_1][library_1].keys():
show_key_dict = dict(show_key_1)
for season in watched_list_1[user_1][library_1][show_key_1]:
for episode in watched_list_1[user_1][library_1][show_key_1][season]:
for episode_key, episode_item in episode.items():
# If episode_key and episode_item are in episode_watched_list_2_keys_dict exactly, then remove from watch_list_1
if episode_key in episode_watched_list_2_keys_dict.keys():
if episode_item in episode_watched_list_2_keys_dict[episode_key]:
if episode in modified_watched_list_1[user_1][library_1][show_key_1][season]:
logger(f"Removing {show_key_dict['title']} {episode} from {library_1}", 3)
modified_watched_list_1[user_1][library_1][show_key_1][season].remove(episode)
# Remove empty seasons
if len(modified_watched_list_1[user_1][library_1][show_key_1][season]) == 0:
if season in modified_watched_list_1[user_1][library_1][show_key_1]:
logger(f"Removing {season} from {library_1} because it is empty", 3)
del modified_watched_list_1[user_1][library_1][show_key_1][season]
# If the show is empty, remove the show
if len(modified_watched_list_1[user_1][library_1][show_key_1]) == 0:
if show_key_1 in modified_watched_list_1[user_1][library_1]:
logger(f"Removing {show_key_dict['title']} from {library_1} because it is empty", 1)
del modified_watched_list_1[user_1][library_1][show_key_1]
for user_1 in watched_list_1:
for library_1 in watched_list_1[user_1]:
if library_1 in modified_watched_list_1[user_1]:
# If library is empty then remove it
if len(modified_watched_list_1[user_1][library_1]) == 0:
logger(f"Removing {library_1} from {user_1} because it is empty", 1)
del modified_watched_list_1[user_1][library_1]
if user_1 in modified_watched_list_1:
# If user is empty delete user
if len(modified_watched_list_1[user_1]) == 0:
logger(f"Removing {user_1} from watched list 1 because it is empty", 1)
del modified_watched_list_1[user_1]
return modified_watched_list_1
def setup_black_white_lists(library_mapping=None):
blacklist_library = os.getenv("BLACKLIST_LIBRARY")
if blacklist_library:
if len(blacklist_library) > 0:
blacklist_library = blacklist_library.split(",")
blacklist_library = [x.strip() for x in blacklist_library]
if library_mapping:
temp_library = []
for library in blacklist_library:
library_other = search_mapping(library_mapping, library)
if library_other:
temp_library.append(library_other)
blacklist_library = blacklist_library + temp_library
else:
blacklist_library = []
logger(f"Blacklist Library: {blacklist_library}", 1)
whitelist_library = os.getenv("WHITELIST_LIBRARY")
if whitelist_library:
if len(whitelist_library) > 0:
whitelist_library = whitelist_library.split(",")
whitelist_library = [x.strip() for x in whitelist_library]
if library_mapping:
temp_library = []
for library in whitelist_library:
library_other = search_mapping(library_mapping, library)
if library_other:
temp_library.append(library_other)
whitelist_library = whitelist_library + temp_library
else:
whitelist_library = []
logger(f"Whitelist Library: {whitelist_library}", 1)
blacklist_library_type = os.getenv("BLACKLIST_LIBRARY_TYPE")
if blacklist_library_type:
if len(blacklist_library_type) > 0:
blacklist_library_type = blacklist_library_type.split(",")
blacklist_library_type = [x.lower().strip() for x in blacklist_library_type]
else:
blacklist_library_type = []
logger(f"Blacklist Library Type: {blacklist_library_type}", 1)
whitelist_library_type = os.getenv("WHITELIST_LIBRARY_TYPE")
if whitelist_library_type:
if len(whitelist_library_type) > 0:
whitelist_library_type = whitelist_library_type.split(",")
whitelist_library_type = [x.lower().strip() for x in whitelist_library_type]
else:
whitelist_library_type = []
logger(f"Whitelist Library Type: {whitelist_library_type}", 1)
blacklist_users = os.getenv("BLACKLIST_USERS")
if blacklist_users:
if len(blacklist_users) > 0:
blacklist_users = blacklist_users.split(",")
blacklist_users = [x.lower().strip() for x in blacklist_users]
else:
blacklist_users = []
logger(f"Blacklist Users: {blacklist_users}", 1)
whitelist_users = os.getenv("WHITELIST_USERS")
if whitelist_users:
if len(whitelist_users) > 0:
whitelist_users = whitelist_users.split(",")
whitelist_users = [x.lower().strip() for x in whitelist_users]
else:
whitelist_users = []
else:
whitelist_users = []
logger(f"Whitelist Users: {whitelist_users}", 1)
return blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users
def setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mapping=None):
# generate list of users from server 1 and server 2
server_1_type = server_1[0]
server_1_connection = server_1[1]
server_2_type = server_2[0]
server_2_connection = server_2[1]
server_1_users = []
if server_1_type == "plex":
server_1_users = [ x.title.lower() for x in server_1_connection.users ]
elif server_1_type == "jellyfin":
server_1_users = [ key.lower() for key in server_1_connection.users.keys() ]
server_2_users = []
if server_2_type == "plex":
server_2_users = [ x.title.lower() for x in server_2_connection.users ]
elif server_2_type == "jellyfin":
server_2_users = [ key.lower() for key in server_2_connection.users.keys() ]
# combined list of overlapping users from plex and jellyfin
users = {}
for server_1_user in server_1_users:
if user_mapping:
jellyfin_plex_mapped_user = search_mapping(user_mapping, server_1_user)
if jellyfin_plex_mapped_user:
users[server_1_user] = jellyfin_plex_mapped_user
continue
if server_1_user in server_2_users:
users[server_1_user] = server_1_user
for server_2_user in server_2_users:
if user_mapping:
plex_jellyfin_mapped_user = search_mapping(user_mapping, server_2_user)
if plex_jellyfin_mapped_user:
users[plex_jellyfin_mapped_user] = server_2_user
continue
if server_2_user in server_1_users:
users[server_2_user] = server_2_user
logger(f"User list that exist on both servers {users}", 1)
users_filtered = {}
for user in users:
# whitelist_user is not empty and user lowercase is not in whitelist lowercase
if len(whitelist_users) > 0:
if user not in whitelist_users and users[user] not in whitelist_users:
logger(f"{user} or {users[user]} is not in whitelist", 1)
continue
if user not in blacklist_users and users[user] not in blacklist_users:
users_filtered[user] = users[user]
logger(f"Filtered user list {users_filtered}", 1)
if server_1_type == "plex":
output_server_1_users = []
for plex_user in server_1_connection.users:
if plex_user.title.lower() in users_filtered.keys() or plex_user.title.lower() in users_filtered.values():
output_server_1_users.append(plex_user)
elif server_1_type == "jellyfin":
output_server_1_users = {}
for jellyfin_user, jellyfin_id in server_1_connection.users.items():
if jellyfin_user.lower() in users_filtered.keys() or jellyfin_user.lower() in users_filtered.values():
output_server_1_users[jellyfin_user] = jellyfin_id
if server_2_type == "plex":
output_server_2_users = []
for plex_user in server_2_connection.users:
if plex_user.title.lower() in users_filtered.keys() or plex_user.title.lower() in users_filtered.values():
output_server_2_users.append(plex_user)
elif server_2_type == "jellyfin":
output_server_2_users = {}
for jellyfin_user, jellyfin_id in server_2_connection.users.items():
if jellyfin_user.lower() in users_filtered.keys() or jellyfin_user.lower() in users_filtered.values():
output_server_2_users[jellyfin_user] = jellyfin_id
if len(output_server_1_users) == 0:
raise Exception(f"No users found for server 1, users found {users} filtered users {users_filtered}")
if len(output_server_2_users) == 0:
raise Exception(f"No users found for server 2, users found {users} filtered users {users_filtered}")
logger(f"Server 1 users: {output_server_1_users}", 1)
logger(f"Server 2 users: {output_server_2_users}", 1)
return output_server_1_users, output_server_2_users
def generate_server_connections():
servers = []
plex_baseurl = os.getenv("PLEX_BASEURL", None)
plex_token = os.getenv("PLEX_TOKEN", None)
plex_username = os.getenv("PLEX_USERNAME", None)
plex_password = os.getenv("PLEX_PASSWORD", None)
plex_servername = os.getenv("PLEX_SERVERNAME", None)
if plex_baseurl and plex_token:
plex_baseurl = plex_baseurl.split(",")
plex_token = plex_token.split(",")
if len(plex_baseurl) != len(plex_token):
raise Exception("PLEX_BASEURL and PLEX_TOKEN must have the same number of entries")
for i, url in enumerate(plex_baseurl):
servers.append(("plex", Plex(baseurl=url.strip(), token=plex_token[i].strip(), username=None, password=None, servername=None)))
if plex_username and plex_password and plex_servername:
plex_username = plex_username.split(",")
plex_password = plex_password.split(",")
plex_servername = plex_servername.split(",")
if len(plex_username) != len(plex_password) or len(plex_username) != len(plex_servername):
raise Exception("PLEX_USERNAME, PLEX_PASSWORD and PLEX_SERVERNAME must have the same number of entries")
for i, username in enumerate(plex_username):
servers.append(("plex", Plex(baseurl=None, token=None, username=username.strip(), password=plex_password[i].strip(), servername=plex_servername[i].strip())))
jellyfin_baseurl = os.getenv("JELLYFIN_BASEURL", None)
jellyfin_token = os.getenv("JELLYFIN_TOKEN", None)
if jellyfin_baseurl and jellyfin_token:
jellyfin_baseurl = jellyfin_baseurl.split(",")
jellyfin_token = jellyfin_token.split(",")
if len(jellyfin_baseurl) != len(jellyfin_token):
raise Exception("JELLYFIN_BASEURL and JELLYFIN_TOKEN must have the same number of entries")
for i, baseurl in enumerate(jellyfin_baseurl):
servers.append(("jellyfin", Jellyfin(baseurl=baseurl.strip(), token=jellyfin_token[i].strip())))
return servers
def main():
logfile = os.getenv("LOGFILE","log.log")
# Delete logfile if it exists
if os.path.exists(logfile):
os.remove(logfile)
dryrun = str_to_bool(os.getenv("DRYRUN", "False"))
logger(f"Dryrun: {dryrun}", 1)
user_mapping = os.getenv("USER_MAPPING")
if user_mapping:
user_mapping = json.loads(user_mapping.lower())
logger(f"User Mapping: {user_mapping}", 1)
library_mapping = os.getenv("LIBRARY_MAPPING")
if library_mapping:
library_mapping = json.loads(library_mapping)
logger(f"Library Mapping: {library_mapping}", 1)
# Create (black/white)lists
blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users = setup_black_white_lists(library_mapping)
# Create server connections
servers = generate_server_connections()
for server_1 in servers:
# If server is the final server in the list, then we are done with the loop
if server_1 == servers[-1]:
break
# Start server_2 at the next server in the list
for server_2 in servers[servers.index(server_1) + 1:]:
server_1_connection = server_1[1]
server_2_connection = server_2[1]
# Create users list
server_1_users, server_2_users = setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mapping)
args = [[server_1_connection.get_watched, server_1_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping]
, [server_2_connection.get_watched, server_2_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping]]
results = future_thread_executor(args)
server_1_watched = results[0]
server_2_watched = results[1]
# clone watched so it isnt modified in the cleanup function so all duplicates are actually removed
server_1_watched_filtered = copy.deepcopy(server_1_watched)
server_2_watched_filtered = copy.deepcopy(server_2_watched)
logger("Cleaning Server 1 Watched", 1)
server_1_watched_filtered = cleanup_watched(server_1_watched, server_2_watched, user_mapping, library_mapping)
logger("Cleaning Server 2 Watched", 1)
server_2_watched_filtered = cleanup_watched(server_2_watched, server_1_watched, user_mapping, library_mapping)
logger(f"server 1 watched that needs to be synced to server 2:\n{server_1_watched_filtered}", 1)
logger(f"server 2 watched that needs to be synced to server 1:\n{server_2_watched_filtered}", 1)
args= [[server_1_connection.update_watched, server_2_watched_filtered, user_mapping, library_mapping, dryrun]
, [server_2_connection.update_watched, server_1_watched_filtered, user_mapping, library_mapping, dryrun]]
future_thread_executor(args)
if __name__ == "__main__":
sleep_duration = float(os.getenv("SLEEP_DURATION", "3600"))
while(True):
try:
main() main()
logger(f"Looping in {sleep_duration}")
sleep(sleep_duration)
except Exception as error:
if isinstance(error, list):
for message in error:
logger(message, log_type=2)
else:
logger(error, log_type=2)
logger(traceback.format_exc(), 2)
logger(f"Retrying in {sleep_duration}", log_type=0)
except KeyboardInterrupt:
logger("Exiting", log_type=0)
os._exit(0)

View File

@@ -39,11 +39,11 @@ def search_mapping(dictionary: dict, key_value: str):
if key_value in dictionary.keys(): if key_value in dictionary.keys():
return dictionary[key_value] return dictionary[key_value]
elif key_value.lower() in dictionary.keys(): elif key_value.lower() in dictionary.keys():
return dictionary[key_value] return dictionary[key_value.lower()]
elif key_value in dictionary.values(): elif key_value in dictionary.values():
return list(dictionary.keys())[list(dictionary.values()).index(key_value)] return list(dictionary.keys())[list(dictionary.values()).index(key_value)]
elif key_value.lower() in dictionary.values(): elif key_value.lower() in dictionary.values():
return list(dictionary.keys())[list(dictionary.values()).index(key_value)] return list(dictionary.keys())[list(dictionary.values()).index(key_value.lower())]
else: else:
return None return None
@@ -116,11 +116,14 @@ def generate_library_guids_dict(user_list: dict, generate_output: int):
return show_output_dict, episode_output_dict, movies_output_dict return show_output_dict, episode_output_dict, movies_output_dict
def future_thread_executor(args: list): def future_thread_executor(args: list, workers: int = -1):
futures_list = [] futures_list = []
results = [] results = []
workers=1
if workers == -1:
workers = min(32, os.cpu_count()*1.25)
with ThreadPoolExecutor() as executor: with ThreadPoolExecutor(max_workers=workers) as executor:
for arg in args: for arg in args:
# * arg unpacks the list into actual arguments # * arg unpacks the list into actual arguments
futures_list.append(executor.submit(*arg)) futures_list.append(executor.submit(*arg))

View File

@@ -39,11 +39,13 @@ class Jellyfin():
response = self.session.post(self.baseurl + query, headers=headers) response = self.session.post(self.baseurl + query, headers=headers)
return response.json() return response.json()
except Exception as e: except Exception as e:
logger(e, 2) logger(f"Jellyfin: Query failed {e}", 2)
logger(response, 2) raise Exception(e)
def get_users(self): def get_users(self):
try:
users = {} users = {}
query = "/Users" query = "/Users"
@@ -55,8 +57,13 @@ class Jellyfin():
users[user["Name"]] = user["Id"] users[user["Name"]] = user["Id"]
return users return users
except Exception as e:
logger(f"Jellyfin: Get users failed {e}", 2)
raise Exception(e)
def get_user_watched(self, user_name, user_id, library_type, library_id, library_title): def get_user_watched(self, user_name, user_id, library_type, library_id, library_title):
try:
user_name = user_name.lower()
user_watched = {} user_watched = {}
user_watched[user_name] = {} user_watched[user_name] = {}
@@ -100,9 +107,13 @@ class Jellyfin():
user_watched[user_name][library_title][show_guids][season["Name"]].append(episode["ProviderIds"]) user_watched[user_name][library_title][show_guids][season["Name"]].append(episode["ProviderIds"])
return user_watched return user_watched
except Exception as e:
logger(f"Jellyfin: Failed to get watched for {user_name} in library {library_title}, Error: {e}", 2)
raise Exception(e)
def get_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping=None): def get_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping=None):
try:
users_watched = {} users_watched = {}
args = [] args = []
@@ -138,9 +149,12 @@ class Jellyfin():
users_watched[user].update(user_watched_temp) users_watched[user].update(user_watched_temp)
return users_watched return users_watched
except Exception as e:
logger(f"Jellyfin: Failed to get watched, Error: {e}", 2)
raise Exception(e)
def update_user_watched(self, user, user_id, library, library_id, videos, dryrun): def update_user_watched(self, user, user_id, library, library_id, videos, dryrun):
try:
logger(f"Jellyfin: Updating watched for {user} in library {library}", 1) logger(f"Jellyfin: Updating watched for {user} in library {library}", 1)
library_search = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&ParentId={library_id}&limit=1", "get") library_search = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&ParentId={library_id}&limit=1", "get")
library_type = library_search["Items"][0]["Type"] library_type = library_search["Items"][0]["Type"]
@@ -198,8 +212,13 @@ class Jellyfin():
if show_found: if show_found:
break break
except Exception as e:
logger(f"Jellyfin: Error updating watched for {user} in library {library}", 2)
raise Exception(e)
def update_watched(self, watched_list, user_mapping=None, library_mapping=None, dryrun=False): def update_watched(self, watched_list, user_mapping=None, library_mapping=None, dryrun=False):
try:
args = [] args = []
for user, libraries in watched_list.items(): for user, libraries in watched_list.items():
user_other = None user_other = None
@@ -234,13 +253,16 @@ class Jellyfin():
if library.lower() not in [x["Name"].lower() for x in jellyfin_libraries]: if library.lower() not in [x["Name"].lower() for x in jellyfin_libraries]:
if library_other and library_other.lower() in [x["Name"].lower() for x in jellyfin_libraries]: if library_other:
logger(f"Plex: Library {library} not found, but {library_other} found, using {library_other}", 1) if library_other.lower() in [x["Name"].lower() for x in jellyfin_libraries]:
logger(f"Jellyfin: Library {library} not found, but {library_other} found, using {library_other}", 1)
library = library_other library = library_other
else: else:
logger(f"Library {library} {library_other} not found in Plex library list", 2) logger(f"Jellyfin: Library {library} or {library_other} not found in library list", 2)
continue
else:
logger(f"Jellyfin: Library {library} not found in library list", 2)
continue continue
library_id = None library_id = None
for jellyfin_library in jellyfin_libraries: for jellyfin_library in jellyfin_libraries:
@@ -252,3 +274,6 @@ class Jellyfin():
args.append([self.update_user_watched, user, user_id, library, library_id, videos, dryrun]) args.append([self.update_user_watched, user, user_id, library, library_id, videos, dryrun])
future_thread_executor(args) future_thread_executor(args)
except Exception as e:
logger(f"Jellyfin: Error updating watched", 2)
raise Exception(e)

400
src/main.py Normal file
View File

@@ -0,0 +1,400 @@
import copy, os, traceback, json
from dotenv import load_dotenv
from time import sleep
from src.functions import logger, str_to_bool, search_mapping, generate_library_guids_dict, future_thread_executor
from src.plex import Plex
from src.jellyfin import Jellyfin
load_dotenv(override=True)
def cleanup_watched(watched_list_1, watched_list_2, user_mapping=None, library_mapping=None):
modified_watched_list_1 = copy.deepcopy(watched_list_1)
# remove entries from plex_watched that are in jellyfin_watched
for user_1 in watched_list_1:
user_other = None
if user_mapping:
user_other = search_mapping(user_mapping, user_1)
if user_1 in modified_watched_list_1:
if user_1 in watched_list_2:
user_2 = user_1
elif user_other in watched_list_2:
user_2 = user_other
else:
logger(f"User {user_1} and {user_other} not found in watched list 2", 1)
continue
for library_1 in watched_list_1[user_1]:
library_other = None
if library_mapping:
library_other = search_mapping(library_mapping, library_1)
if library_1 in modified_watched_list_1[user_1]:
if library_1 in watched_list_2[user_2]:
library_2 = library_1
elif library_other in watched_list_2[user_2]:
library_2 = library_other
else:
logger(f"library {library_1} and {library_other} not found in watched list 2", 1)
continue
# Movies
if isinstance(watched_list_1[user_1][library_1], list):
for item in watched_list_1[user_1][library_1]:
for watch_list_1_key, watch_list_1_value in item.items():
for watch_list_2_item in watched_list_2[user_2][library_2]:
for watch_list_2_item_key, watch_list_2_item_value in watch_list_2_item.items():
if watch_list_1_key == watch_list_2_item_key and watch_list_1_value == watch_list_2_item_value:
if item in modified_watched_list_1[user_1][library_1]:
logger(f"Removing {item} from {library_1}", 3)
modified_watched_list_1[user_1][library_1].remove(item)
# TV Shows
elif isinstance(watched_list_1[user_1][library_1], dict):
# Generate full list of provider ids for episodes in watch_list_2 to easily compare if they exist in watch_list_1
_, episode_watched_list_2_keys_dict, _ = generate_library_guids_dict(watched_list_2[user_2][library_2], 1)
for show_key_1 in watched_list_1[user_1][library_1].keys():
show_key_dict = dict(show_key_1)
for season in watched_list_1[user_1][library_1][show_key_1]:
for episode in watched_list_1[user_1][library_1][show_key_1][season]:
for episode_key, episode_item in episode.items():
# If episode_key and episode_item are in episode_watched_list_2_keys_dict exactly, then remove from watch_list_1
if episode_key in episode_watched_list_2_keys_dict.keys():
if episode_item in episode_watched_list_2_keys_dict[episode_key]:
if episode in modified_watched_list_1[user_1][library_1][show_key_1][season]:
logger(f"Removing {show_key_dict['title']} {episode} from {library_1}", 3)
modified_watched_list_1[user_1][library_1][show_key_1][season].remove(episode)
# Remove empty seasons
if len(modified_watched_list_1[user_1][library_1][show_key_1][season]) == 0:
if season in modified_watched_list_1[user_1][library_1][show_key_1]:
logger(f"Removing {season} from {library_1} because it is empty", 3)
del modified_watched_list_1[user_1][library_1][show_key_1][season]
# If the show is empty, remove the show
if len(modified_watched_list_1[user_1][library_1][show_key_1]) == 0:
if show_key_1 in modified_watched_list_1[user_1][library_1]:
logger(f"Removing {show_key_dict['title']} from {library_1} because it is empty", 1)
del modified_watched_list_1[user_1][library_1][show_key_1]
for user_1 in watched_list_1:
for library_1 in watched_list_1[user_1]:
if library_1 in modified_watched_list_1[user_1]:
# If library is empty then remove it
if len(modified_watched_list_1[user_1][library_1]) == 0:
logger(f"Removing {library_1} from {user_1} because it is empty", 1)
del modified_watched_list_1[user_1][library_1]
if user_1 in modified_watched_list_1:
# If user is empty delete user
if len(modified_watched_list_1[user_1]) == 0:
logger(f"Removing {user_1} from watched list 1 because it is empty", 1)
del modified_watched_list_1[user_1]
return modified_watched_list_1
def setup_black_white_lists(library_mapping=None):
blacklist_library = os.getenv("BLACKLIST_LIBRARY")
if blacklist_library:
if len(blacklist_library) > 0:
blacklist_library = blacklist_library.split(",")
blacklist_library = [x.strip() for x in blacklist_library]
if library_mapping:
temp_library = []
for library in blacklist_library:
library_other = search_mapping(library_mapping, library)
if library_other:
temp_library.append(library_other)
blacklist_library = blacklist_library + temp_library
else:
blacklist_library = []
logger(f"Blacklist Library: {blacklist_library}", 1)
whitelist_library = os.getenv("WHITELIST_LIBRARY")
if whitelist_library:
if len(whitelist_library) > 0:
whitelist_library = whitelist_library.split(",")
whitelist_library = [x.strip() for x in whitelist_library]
if library_mapping:
temp_library = []
for library in whitelist_library:
library_other = search_mapping(library_mapping, library)
if library_other:
temp_library.append(library_other)
whitelist_library = whitelist_library + temp_library
else:
whitelist_library = []
logger(f"Whitelist Library: {whitelist_library}", 1)
blacklist_library_type = os.getenv("BLACKLIST_LIBRARY_TYPE")
if blacklist_library_type:
if len(blacklist_library_type) > 0:
blacklist_library_type = blacklist_library_type.split(",")
blacklist_library_type = [x.lower().strip() for x in blacklist_library_type]
else:
blacklist_library_type = []
logger(f"Blacklist Library Type: {blacklist_library_type}", 1)
whitelist_library_type = os.getenv("WHITELIST_LIBRARY_TYPE")
if whitelist_library_type:
if len(whitelist_library_type) > 0:
whitelist_library_type = whitelist_library_type.split(",")
whitelist_library_type = [x.lower().strip() for x in whitelist_library_type]
else:
whitelist_library_type = []
logger(f"Whitelist Library Type: {whitelist_library_type}", 1)
blacklist_users = os.getenv("BLACKLIST_USERS")
if blacklist_users:
if len(blacklist_users) > 0:
blacklist_users = blacklist_users.split(",")
blacklist_users = [x.lower().strip() for x in blacklist_users]
else:
blacklist_users = []
logger(f"Blacklist Users: {blacklist_users}", 1)
whitelist_users = os.getenv("WHITELIST_USERS")
if whitelist_users:
if len(whitelist_users) > 0:
whitelist_users = whitelist_users.split(",")
whitelist_users = [x.lower().strip() for x in whitelist_users]
else:
whitelist_users = []
else:
whitelist_users = []
logger(f"Whitelist Users: {whitelist_users}", 1)
return blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users
def setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mapping=None):
# generate list of users from server 1 and server 2
server_1_type = server_1[0]
server_1_connection = server_1[1]
server_2_type = server_2[0]
server_2_connection = server_2[1]
server_1_users = []
if server_1_type == "plex":
server_1_users = [ x.title.lower() for x in server_1_connection.users ]
elif server_1_type == "jellyfin":
server_1_users = [ key.lower() for key in server_1_connection.users.keys() ]
server_2_users = []
if server_2_type == "plex":
server_2_users = [ x.title.lower() for x in server_2_connection.users ]
elif server_2_type == "jellyfin":
server_2_users = [ key.lower() for key in server_2_connection.users.keys() ]
# combined list of overlapping users from plex and jellyfin
users = {}
for server_1_user in server_1_users:
if user_mapping:
jellyfin_plex_mapped_user = search_mapping(user_mapping, server_1_user)
if jellyfin_plex_mapped_user:
users[server_1_user] = jellyfin_plex_mapped_user
continue
if server_1_user in server_2_users:
users[server_1_user] = server_1_user
for server_2_user in server_2_users:
if user_mapping:
plex_jellyfin_mapped_user = search_mapping(user_mapping, server_2_user)
if plex_jellyfin_mapped_user:
users[plex_jellyfin_mapped_user] = server_2_user
continue
if server_2_user in server_1_users:
users[server_2_user] = server_2_user
logger(f"User list that exist on both servers {users}", 1)
users_filtered = {}
for user in users:
# whitelist_user is not empty and user lowercase is not in whitelist lowercase
if len(whitelist_users) > 0:
if user not in whitelist_users and users[user] not in whitelist_users:
logger(f"{user} or {users[user]} is not in whitelist", 1)
continue
if user not in blacklist_users and users[user] not in blacklist_users:
users_filtered[user] = users[user]
logger(f"Filtered user list {users_filtered}", 1)
if server_1_type == "plex":
output_server_1_users = []
for plex_user in server_1_connection.users:
if plex_user.title.lower() in users_filtered.keys() or plex_user.title.lower() in users_filtered.values():
output_server_1_users.append(plex_user)
elif server_1_type == "jellyfin":
output_server_1_users = {}
for jellyfin_user, jellyfin_id in server_1_connection.users.items():
if jellyfin_user.lower() in users_filtered.keys() or jellyfin_user.lower() in users_filtered.values():
output_server_1_users[jellyfin_user] = jellyfin_id
if server_2_type == "plex":
output_server_2_users = []
for plex_user in server_2_connection.users:
if plex_user.title.lower() in users_filtered.keys() or plex_user.title.lower() in users_filtered.values():
output_server_2_users.append(plex_user)
elif server_2_type == "jellyfin":
output_server_2_users = {}
for jellyfin_user, jellyfin_id in server_2_connection.users.items():
if jellyfin_user.lower() in users_filtered.keys() or jellyfin_user.lower() in users_filtered.values():
output_server_2_users[jellyfin_user] = jellyfin_id
if len(output_server_1_users) == 0:
raise Exception(f"No users found for server 1, users found {users} filtered users {users_filtered}")
if len(output_server_2_users) == 0:
raise Exception(f"No users found for server 2, users found {users} filtered users {users_filtered}")
logger(f"Server 1 users: {output_server_1_users}", 1)
logger(f"Server 2 users: {output_server_2_users}", 1)
return output_server_1_users, output_server_2_users
def generate_server_connections():
servers = []
plex_baseurl = os.getenv("PLEX_BASEURL", None)
plex_token = os.getenv("PLEX_TOKEN", None)
plex_username = os.getenv("PLEX_USERNAME", None)
plex_password = os.getenv("PLEX_PASSWORD", None)
plex_servername = os.getenv("PLEX_SERVERNAME", None)
if plex_baseurl and plex_token:
plex_baseurl = plex_baseurl.split(",")
plex_token = plex_token.split(",")
if len(plex_baseurl) != len(plex_token):
raise Exception("PLEX_BASEURL and PLEX_TOKEN must have the same number of entries")
for i, url in enumerate(plex_baseurl):
servers.append(("plex", Plex(baseurl=url.strip(), token=plex_token[i].strip(), username=None, password=None, servername=None)))
if plex_username and plex_password and plex_servername:
plex_username = plex_username.split(",")
plex_password = plex_password.split(",")
plex_servername = plex_servername.split(",")
if len(plex_username) != len(plex_password) or len(plex_username) != len(plex_servername):
raise Exception("PLEX_USERNAME, PLEX_PASSWORD and PLEX_SERVERNAME must have the same number of entries")
for i, username in enumerate(plex_username):
servers.append(("plex", Plex(baseurl=None, token=None, username=username.strip(), password=plex_password[i].strip(), servername=plex_servername[i].strip())))
jellyfin_baseurl = os.getenv("JELLYFIN_BASEURL", None)
jellyfin_token = os.getenv("JELLYFIN_TOKEN", None)
if jellyfin_baseurl and jellyfin_token:
jellyfin_baseurl = jellyfin_baseurl.split(",")
jellyfin_token = jellyfin_token.split(",")
if len(jellyfin_baseurl) != len(jellyfin_token):
raise Exception("JELLYFIN_BASEURL and JELLYFIN_TOKEN must have the same number of entries")
for i, baseurl in enumerate(jellyfin_baseurl):
servers.append(("jellyfin", Jellyfin(baseurl=baseurl.strip(), token=jellyfin_token[i].strip())))
return servers
def main_loop():
logfile = os.getenv("LOGFILE","log.log")
# Delete logfile if it exists
if os.path.exists(logfile):
os.remove(logfile)
dryrun = str_to_bool(os.getenv("DRYRUN", "False"))
logger(f"Dryrun: {dryrun}", 1)
user_mapping = os.getenv("USER_MAPPING")
if user_mapping:
user_mapping = json.loads(user_mapping.lower())
logger(f"User Mapping: {user_mapping}", 1)
library_mapping = os.getenv("LIBRARY_MAPPING")
if library_mapping:
library_mapping = json.loads(library_mapping)
logger(f"Library Mapping: {library_mapping}", 1)
# Create (black/white)lists
blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users = setup_black_white_lists(library_mapping)
# Create server connections
servers = generate_server_connections()
for server_1 in servers:
# If server is the final server in the list, then we are done with the loop
if server_1 == servers[-1]:
break
# Start server_2 at the next server in the list
for server_2 in servers[servers.index(server_1) + 1:]:
server_1_connection = server_1[1]
server_2_connection = server_2[1]
# Create users list
server_1_users, server_2_users = setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mapping)
args = [[server_1_connection.get_watched, server_1_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping]
, [server_2_connection.get_watched, server_2_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping]]
results = future_thread_executor(args)
server_1_watched = results[0]
server_2_watched = results[1]
logger(f"Server 1 watched: {server_1_watched}", 3)
logger(f"Server 2 watched: {server_2_watched}", 3)
# clone watched so it isnt modified in the cleanup function so all duplicates are actually removed
server_1_watched_filtered = copy.deepcopy(server_1_watched)
server_2_watched_filtered = copy.deepcopy(server_2_watched)
logger("Cleaning Server 1 Watched", 1)
server_1_watched_filtered = cleanup_watched(server_1_watched, server_2_watched, user_mapping, library_mapping)
logger("Cleaning Server 2 Watched", 1)
server_2_watched_filtered = cleanup_watched(server_2_watched, server_1_watched, user_mapping, library_mapping)
logger(f"server 1 watched that needs to be synced to server 2:\n{server_1_watched_filtered}", 1)
logger(f"server 2 watched that needs to be synced to server 1:\n{server_2_watched_filtered}", 1)
args= [[server_1_connection.update_watched, server_2_watched_filtered, user_mapping, library_mapping, dryrun]
, [server_2_connection.update_watched, server_1_watched_filtered, user_mapping, library_mapping, dryrun]]
future_thread_executor(args)
def main():
sleep_duration = float(os.getenv("SLEEP_DURATION", "3600"))
while(True):
try:
main_loop()
logger(f"Looping in {sleep_duration}")
sleep(sleep_duration)
except Exception as error:
if isinstance(error, list):
for message in error:
logger(message, log_type=2)
else:
logger(error, log_type=2)
logger(traceback.format_exc(), 2)
logger(f"Retrying in {sleep_duration}", log_type=0)
except KeyboardInterrupt:
logger("Exiting", log_type=0)
os._exit(0)

View File

@@ -37,25 +37,31 @@ class Plex:
logger(f"Plex: Failed to login, {msg}, Error: {e}", 2) logger(f"Plex: Failed to login, {msg}, Error: {e}", 2)
else: else:
logger(f"Plex: Failed to login, Error: {e}", 2) logger(f"Plex: Failed to login, Error: {e}", 2)
return None raise Exception(e)
def get_users(self): def get_users(self):
try:
users = self.plex.myPlexAccount().users() users = self.plex.myPlexAccount().users()
# append self to users # append self to users
users.append(self.plex.myPlexAccount()) users.append(self.plex.myPlexAccount())
return users return users
except Exception as e:
logger(f"Plex: Failed to get users, Error: {e}", 2)
raise Exception(e)
def get_user_watched(self, user, user_plex, library): def get_user_watched(self, user, user_plex, library):
try:
user_name = user.title.lower()
user_watched = {} user_watched = {}
user_watched[user.title] = {} user_watched[user_name] = {}
logger(f"Plex: Generating watched for {user.title} in library {library.title}", 0) logger(f"Plex: Generating watched for {user_name} in library {library.title}", 0)
if library.type == "movie": if library.type == "movie":
user_watched[user.title][library.title] = [] user_watched[user_name][library.title] = []
library_videos = user_plex.library.section(library.title) library_videos = user_plex.library.section(library.title)
for video in library_videos.search(unmatched=False, unwatched=False): for video in library_videos.search(unmatched=False, unwatched=False):
@@ -64,10 +70,10 @@ class Plex:
guid_source = re.search(r'(.*)://', guid.id).group(1).lower() guid_source = re.search(r'(.*)://', guid.id).group(1).lower()
guid_id = re.search(r'://(.*)', guid.id).group(1) guid_id = re.search(r'://(.*)', guid.id).group(1)
guids[guid_source] = guid_id guids[guid_source] = guid_id
user_watched[user.title][library.title].append(guids) user_watched[user_name][library.title].append(guids)
elif library.type == "show": elif library.type == "show":
user_watched[user.title][library.title] = {} user_watched[user_name][library.title] = {}
library_videos = user_plex.library.section(library.title) library_videos = user_plex.library.section(library.title)
for show in library_videos.search(unmatched=False, unwatched=False): for show in library_videos.search(unmatched=False, unwatched=False):
@@ -95,16 +101,21 @@ class Plex:
if episode_guids: if episode_guids:
# append show, season, episode # append show, season, episode
if show_guids not in user_watched[user.title][library.title]: if show_guids not in user_watched[user_name][library.title]:
user_watched[user.title][library.title][show_guids] = {} user_watched[user_name][library.title][show_guids] = {}
if season.title not in user_watched[user.title][library.title][show_guids]: if season.title not in user_watched[user_name][library.title][show_guids]:
user_watched[user.title][library.title][show_guids][season.title] = {} user_watched[user_name][library.title][show_guids][season.title] = {}
user_watched[user.title][library.title][show_guids][season.title] = episode_guids user_watched[user_name][library.title][show_guids][season.title] = episode_guids
return user_watched return user_watched
except Exception as e:
logger(f"Plex: Failed to get watched for {user_name} in library {library.title}, Error: {e}", 2)
raise Exception(e)
def get_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping): def get_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping):
try:
# Get all libraries # Get all libraries
users_watched = {} users_watched = {}
args = [] args = []
@@ -136,8 +147,13 @@ class Plex:
users_watched[user].update(user_watched_temp) users_watched[user].update(user_watched_temp)
return users_watched return users_watched
except Exception as e:
logger(f"Plex: Failed to get watched, Error: {e}", 2)
raise Exception(e)
def update_user_watched (self, user, user_plex, library, videos, dryrun): def update_user_watched (self, user, user_plex, library, videos, dryrun):
try:
logger(f"Plex: Updating watched for {user.title} in library {library}", 1) logger(f"Plex: Updating watched for {user.title} in library {library}", 1)
library_videos = user_plex.library.section(library) library_videos = user_plex.library.section(library)
@@ -193,10 +209,13 @@ class Plex:
if show_found: if show_found:
break break
except Exception as e:
logger(f"Plex: Failed to update watched for {user.title} in library {library}, Error: {e}", 2)
raise Exception(e)
def update_watched(self, watched_list, user_mapping=None, library_mapping=None, dryrun=False): def update_watched(self, watched_list, user_mapping=None, library_mapping=None, dryrun=False):
try:
args = [] args = []
for user, libraries in watched_list.items(): for user, libraries in watched_list.items():
@@ -232,14 +251,21 @@ class Plex:
# if library in plex library list # if library in plex library list
library_list = user_plex.library.sections() library_list = user_plex.library.sections()
if library.lower() not in [x.title.lower() for x in library_list]: if library.lower() not in [x.title.lower() for x in library_list]:
if library_other and library_other.lower() in [x.title.lower() for x in library_list]: if library_other:
if library_other.lower() in [x.title.lower() for x in library_list]:
logger(f"Plex: Library {library} not found, but {library_other} found, using {library_other}", 1) logger(f"Plex: Library {library} not found, but {library_other} found, using {library_other}", 1)
library = library_other library = library_other
else: else:
logger(f"Library {library} {library_other} not found in Plex library list", 2) logger(f"Plex: Library {library} or {library_other} not found in library list", 2)
continue
else:
logger(f"Plex: Library {library} not found in library list", 2)
continue continue
args.append([self.update_user_watched, user, user_plex, library, videos, dryrun]) args.append([self.update_user_watched, user, user_plex, library, videos, dryrun])
future_thread_executor(args) future_thread_executor(args)
except Exception as e:
logger(f"Plex: Failed to update watched, Error: {e}", 2)
raise Exception(e)