Support x many servers of any combination

pull/17/head
Luigi311 2022-06-13 22:30:41 -06:00
parent 7ef2986bde
commit 7695994ec2
6 changed files with 192 additions and 114 deletions

View File

@ -2,8 +2,8 @@
DRYRUN = "True"
## Additional logging information
DEBUG = "True"
## Debugging level, INFO is default, DEBUG is more verbose
DEBUG_LEVEL = "INFO"
## Debugging level, "info" is default, "debug" is more verbose
DEBUG_LEVEL = "info"
## How often to run the script in seconds
SLEEP_DURATION = "3600"
## Log file where all output will be written to
@ -16,6 +16,7 @@ LOGFILE = "log.log"
## Recommended to use token as it is faster to connect as it is direct to the server instead of going through the plex servers
## URL of the plex server, use hostname or IP address if the hostname is not resolving correctly
## Comma seperated list for multiple servers
PLEX_BASEURL = "http://localhost:32400"
## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
PLEX_TOKEN = "SuperSecretToken"
@ -26,6 +27,7 @@ PLEX_TOKEN = "SuperSecretToken"
## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly
## Comma seperated list for multiple servers
JELLYFIN_BASEURL = "http://localhost:8096"
## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key
JELLYFIN_TOKEN = "SuperSecretToken"

View File

@ -6,7 +6,11 @@ Sync watched between jellyfin and plex
## Description
Keep in sync all your users watched history between jellyfin and plex locally. This uses the imdb ids and any other matching id to find the correct episode/movie between the two. This is not perfect but it works for most cases.
Keep in sync all your users watched history between jellyfin and plex servers locally. This uses the imdb ids and any other matching id to find the correct episode/movie between the two. This is not perfect but it works for most cases. You can use this for as many servers as you want by enterying multiple options in the .env plex/jellyfin section seperated by commas.
## Configuration
## Installation

185
main.py
View File

@ -168,34 +168,49 @@ def setup_black_white_lists(library_mapping=None):
return blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users
def setup_users(plex, jellyfin, blacklist_users, whitelist_users, user_mapping=None):
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() ]
# generate list of users from plex.users
plex_users = [ x.title.lower() for x in plex.users ]
jellyfin_users = [ key.lower() for key in jellyfin.users.keys() ]
# combined list of overlapping users from plex and jellyfin
users = {}
for plex_user in plex_users:
for server_1_user in server_1_users:
if user_mapping:
jellyfin_plex_mapped_user = search_mapping(user_mapping, plex_user)
jellyfin_plex_mapped_user = search_mapping(user_mapping, server_1_user)
if jellyfin_plex_mapped_user:
users[plex_user] = jellyfin_plex_mapped_user
users[server_1_user] = jellyfin_plex_mapped_user
continue
if plex_user in jellyfin_users:
users[plex_user] = plex_user
if server_1_user in server_2_users:
users[server_1_user] = server_1_user
for jellyfin_user in jellyfin_users:
for server_2_user in server_2_users:
if user_mapping:
plex_jellyfin_mapped_user = search_mapping(user_mapping, jellyfin_user)
plex_jellyfin_mapped_user = search_mapping(user_mapping, server_2_user)
if plex_jellyfin_mapped_user:
users[plex_jellyfin_mapped_user] = jellyfin_user
users[plex_jellyfin_mapped_user] = server_2_user
continue
if jellyfin_user in plex_users:
users[jellyfin_user] = jellyfin_user
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)
@ -212,26 +227,84 @@ def setup_users(plex, jellyfin, blacklist_users, whitelist_users, user_mapping=N
logger(f"Filtered user list {users_filtered}", 1)
plex_users = []
for plex_user in plex.users:
if plex_user.title.lower() in users_filtered.keys() or plex_user.title.lower() in users_filtered.values():
plex_users.append(plex_user)
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
jellyfin_users = {}
for jellyfin_user, jellyfin_id in jellyfin.users.items():
if jellyfin_user.lower() in users_filtered.keys() or jellyfin_user.lower() in users_filtered.values():
jellyfin_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(plex_users) == 0:
raise Exception(f"No plex users found, users found {users} filtered users {users_filtered}")
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(jellyfin_users) == 0:
raise Exception(f"No jellyfin users found, 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"plex_users: {plex_users}", 1)
logger(f"jellyfin_users: {jellyfin_users}", 1)
logger(f"Server 1 users: {output_server_1_users}", 1)
logger(f"Server 2 users: {output_server_2_users}", 1)
return plex_users, jellyfin_users
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 in range(len(plex_baseurl)):
servers.append(("plex", Plex(baseurl=plex_baseurl[i].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 in range(len(plex_username)):
servers.append(("plex", Plex(baseurl=None, token=None, username=plex_username[i].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 in range(len(jellyfin_baseurl)):
servers.append(("jellyfin", Jellyfin(baseurl=jellyfin_baseurl[i].strip(), token=jellyfin_token[i].strip())))
print(f"Servers: {servers}")
return servers
def main():
logfile = os.getenv("LOGFILE","log.log")
@ -252,34 +325,50 @@ def main():
library_mapping = json.loads(library_mapping)
logger(f"Library Mapping: {library_mapping}", 1)
plex = Plex()
jellyfin = Jellyfin()
# 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 users list
plex_users, jellyfin_users = setup_users(plex, jellyfin, blacklist_users, whitelist_users, user_mapping)
# Create server connections
servers = generate_server_connections()
plex_watched = plex.get_plex_watched(plex_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping)
jellyfin_watched = jellyfin.get_jellyfin_watched(jellyfin_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping)
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
# clone watched so it isnt modified in the cleanup function so all duplicates are actually removed
plex_watched_filtered = copy.deepcopy(plex_watched)
jellyfin_watched_filtered = copy.deepcopy(jellyfin_watched)
# Start server_2 at the next server in the list
servers_2_ = servers[servers.index(server_1) + 1:]
for server_2 in servers[servers.index(server_1) + 1:]:
print(f"server_1: {server_1}, server_2: {server_2}")
logger("Cleaning Plex Watched", 1)
plex_watched = cleanup_watched(plex_watched_filtered, jellyfin_watched_filtered, user_mapping, library_mapping)
server_1_type = server_1[0]
server_1_connection = server_1[1]
logger("Cleaning Jellyfin Watched", 1)
jellyfin_watched = cleanup_watched(jellyfin_watched_filtered, plex_watched_filtered, user_mapping, library_mapping)
server_2_type = server_2[0]
server_2_connection = server_2[1]
logger(f"plex_watched that needs to be synced to jellyfin:\n{plex_watched}", 1)
logger(f"jellyfin_watched that needs to be synced to plex:\n{jellyfin_watched}", 1)
# Create users list
server_1_users, server_2_users = setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mapping)
# Update watched status
plex.update_watched(jellyfin_watched, user_mapping, library_mapping, dryrun)
jellyfin.update_watched(plex_watched, user_mapping, library_mapping, dryrun)
server_1_watched = server_1_connection.get_watched(server_1_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping)
server_2_watched = server_2_connection.get_watched(server_2_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping)
# 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)
# Update watched status
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)
if __name__ == "__main__":

View File

@ -6,16 +6,16 @@ logfile = os.getenv("LOGFILE","log.log")
def logger(message, log_type=0):
debug = str_to_bool(os.getenv("DEBUG", "True"))
debug_level = os.getenv("DEBUG_LEVEL", "INFO")
debug_level = os.getenv("DEBUG_LEVEL", "info").lower()
output = str(message)
if log_type == 0:
pass
elif log_type == 1 and (debug or debug_level == "INFO"):
elif log_type == 1 and (debug or debug_level == "info"):
output = f"[INFO]: {output}"
elif log_type == 2:
output = f"[ERROR]: {output}"
elif log_type == 3 and (debug and debug_level == "DEBUG"):
elif log_type == 3 and (debug and debug_level == "debug"):
output = f"[DEBUG]: {output}"
else:
output = None

View File

@ -1,16 +1,10 @@
import requests, os
from dotenv import load_dotenv
import requests
from src.functions import logger, search_mapping, str_to_bool, check_skip_logic, generate_library_guids_dict
load_dotenv(override=True)
jellyfin_baseurl = os.getenv("JELLYFIN_BASEURL")
jellyfin_token = os.getenv("JELLYFIN_TOKEN")
class Jellyfin():
def __init__(self):
self.baseurl = jellyfin_baseurl
self.token = jellyfin_token
def __init__(self, baseurl, token):
self.baseurl = baseurl
self.token = token
if not self.baseurl:
raise Exception("Jellyfin baseurl not set")
@ -56,7 +50,7 @@ class Jellyfin():
return users
def get_jellyfin_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):
users_watched = {}
for user_name, user_id in users.items():
@ -131,46 +125,45 @@ class Jellyfin():
def update_watched(self, watched_list, user_mapping=None, library_mapping=None, dryrun=False):
for user, libraries in watched_list.items():
user_other = None
if user_mapping:
user_other = None
if user in user_mapping.keys():
user_other = user_mapping[user]
elif user in user_mapping.values():
user_other = search_mapping(user_mapping, user)
if user_other:
logger(f"Swapping user {user} with {user_other}", 1)
user = user_other
user_id = None
for key in self.users.keys():
if user.lower() == key.lower():
user_id = self.users[key]
break
elif user_other and user_other.lower() == key.lower():
user_id = self.users[key]
break
if not user_id:
logger(f"{user} not found in Jellyfin", 2)
logger(f"{user} {user_other} not found in Jellyfin", 2)
break
jellyfin_libraries = self.query(f"/Users/{user_id}/Views", "get")["Items"]
for library, videos in libraries.items():
library_other = None
if library_mapping:
library_other = None
if library in library_mapping.keys():
library_other = library_mapping[library]
elif library in library_mapping.values():
library_other = search_mapping(library_mapping, library)
if library_other:
logger(f"Swapping library {library} with {library_other}", 1)
library = library_other
if library not in [x["Name"] for x in jellyfin_libraries]:
logger(f"{library} not found in Jellyfin", 2)
continue
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]:
logger(f"Plex: Library {library} not found, but {library_other} found, using {library_other}", 1)
library = library_other
else:
logger(f"Library {library} {library_other} not found in Plex library list", 2)
continue
library_id = None
for jellyfin_library in jellyfin_libraries:

View File

@ -1,31 +1,22 @@
import re, os
from dotenv import load_dotenv
import re
from src.functions import logger, search_mapping, check_skip_logic, generate_library_guids_dict
from plexapi.server import PlexServer
from plexapi.myplex import MyPlexAccount
load_dotenv(override=True)
plex_baseurl = os.getenv("PLEX_BASEURL")
plex_token = os.getenv("PLEX_TOKEN")
username = os.getenv("PLEX_USERNAME")
password = os.getenv("PLEX_PASSWORD")
servername = os.getenv("PLEX_SERVERNAME")
# class plex accept base url and token and username and password but default with none
class Plex:
def __init__(self):
self.baseurl = plex_baseurl
self.token = plex_token
def __init__(self, baseurl=None, token=None, username=None, password=None, servername=None):
self.baseurl = baseurl
self.token = token
self.username = username
self.password = password
self.servername = servername
self.plex = self.plex_login()
self.plex = self.login()
self.admin_user = self.plex.myPlexAccount()
self.users = self.get_plex_users()
self.users = self.get_users()
def plex_login(self):
def login(self):
try:
if self.baseurl and self.token:
# Login via token
@ -47,7 +38,7 @@ class Plex:
return None
def get_plex_users(self):
def get_users(self):
users = self.plex.myPlexAccount().users()
# append self to users
@ -55,7 +46,7 @@ class Plex:
return users
def get_plex_user_watched(self, user, library):
def get_user_watched(self, user, library):
if self.admin_user == user:
user_plex = self.plex
else:
@ -110,7 +101,7 @@ class Plex:
return watched
def get_plex_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):
# Get all libraries
libraries = self.plex.library.sections()
users_watched = {}
@ -129,7 +120,7 @@ class Plex:
for user in users:
logger(f"Plex: Generating watched for {user.title} in library {library_title}", 0)
user_name = user.title.lower()
watched = self.get_plex_user_watched(user, library)
watched = self.get_user_watched(user, library)
if watched:
if user_name not in users_watched:
users_watched[user_name] = {}
@ -141,22 +132,21 @@ class Plex:
def update_watched(self, watched_list, user_mapping=None, library_mapping=None, dryrun=False):
for user, libraries in watched_list.items():
user_other = None
# If type of user is dict
if user_mapping:
user_other = None
if user in user_mapping.keys():
user_other = user_mapping[user]
elif user in user_mapping.values():
user_other = search_mapping(user_mapping, user)
if user_other:
logger(f"Swapping user {user} with {user_other}", 1)
user = user_other
for index, value in enumerate(self.users):
if user.lower() == value.title.lower():
user = self.users[index]
break
elif user_other and user_other.lower() == value.title.lower():
user = self.users[index]
break
if self.admin_user == user:
user_plex = self.plex
@ -164,23 +154,22 @@ class Plex:
user_plex = PlexServer(self.baseurl, user.get_token(self.plex.machineIdentifier))
for library, videos in libraries.items():
library_other = None
if library_mapping:
library_other = None
if library in library_mapping.keys():
library_other = library_mapping[library]
elif library in library_mapping.values():
library_other = search_mapping(library_mapping, library)
if library_other:
logger(f"Swapping library {library} with {library_other}", 1)
library = library_other
# if library in plex library list
library_list = user_plex.library.sections()
if library.lower() not in [x.title.lower() for x in library_list]:
logger(f"Library {library} not found in Plex library list", 2)
continue
if library_other and 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)
library = library_other
else:
logger(f"Library {library} {library_other} not found in Plex library list", 2)
continue
logger(f"Plex: Updating watched for {user.title} in library {library}", 1)
library_videos = user_plex.library.section(library)
@ -191,6 +180,7 @@ class Plex:
for movie_guid in movies_search.guids:
movie_guid_source = re.search(r'(.*)://', movie_guid.id).group(1).lower()
movie_guid_id = re.search(r'://(.*)', movie_guid.id).group(1)
# If movie provider source and movie provider id are in videos_movie_ids exactly, then the movie is in the list
if movie_guid_source in videos_movies_ids.keys():
if movie_guid_id in videos_movies_ids[movie_guid_source]: