""" Author: Fredwuz (frederic23.mai@gmail.com) sync.py (c) 2025 Desc: Sync Overseerr Requests to Plex Collections Created: 6/28/2025 Modified: 7/1/2025 """ import os import re from dotenv import load_dotenv from plexapi.server import PlexServer from plexapi.myplex import MyPlexAccount import requests from urllib.parse import unquote, quote from pathlib import Path # === CONFIGURATION === # Path to the .env file env_path = Path('.') / '.env' visibility_map = { "none": (False, False, False), "all": (True, True, True), "admin": (False, True, False), "shared": (False, False, True), } # Template with empty values env_template = """# Plex configuration # URL of your Plex server (e.g., http://localhost:32400) PLEX_URL= # Plex authentication token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/ PLEX_TOKEN= # Name of the TV section in Plex PLEX_TV_SECTION_NAME= # Name of the Movie section in Plex PLEX_MOVIE_SECTION_NAME= # Name of the prefix for collection to create in Plex (optional, default is 'Requested by') PLEX_COLLECTION_NAME= # Visibility of the collection in Plex !This feature requires a Plex Pass! (optional, default is 'none') [none, all, admin, shared] https://support.plex.tv/articles/manage-recommendations/#toc-2 # Currently there is a bug that if you set the visibility to 'shared' the collection will be visible to all users. # https://forums.plex.tv/t/collections-displayed-in-friends-home-but-is-restricted-using-labels/924590 PLEX_COLLECTION_VISIBILITY= # Overseerr configuration # URL of your Overseerr instance (e.g., http://localhost:5055) OVERSEERR_URL= # API key for Overseerr (found in settings) OVERSEERR_API_KEY= # Number of requests to fetch per page (optional, default is 500) OVERSEERR_PAGE_SIZE= """ # Create .env with empty values if it doesn't exist if not env_path.exists(): print("Creating .env file with empty variables...") with open(env_path, 'w') as f: f.write(env_template) load_dotenv(dotenv_path=env_path) env_vars = ["PLEX_URL","PLEX_TOKEN","PLEX_TV_SECTION_NAME","PLEX_MOVIE_SECTION_NAME","OVERSEERR_URL","OVERSEERR_API_KEY"] empty_vars = [var for var in env_vars if not os.getenv(var)] if empty_vars: print("These environment variables are empty or missing:") for var in empty_vars: print(f"- {var}") quit("Stopping execution due to missing environment variables.") # Plex configuration PLEX_URL = os.getenv('PLEX_URL') PLEX_TOKEN = os.getenv('PLEX_TOKEN') PLEX_TV_SECTION_NAME = os.getenv('PLEX_TV_SECTION_NAME') PLEX_MOVIE_SECTION_NAME = os.getenv('PLEX_MOVIE_SECTION_NAME') PLEX_COLLECTION_NAME = os.getenv('PLEX_COLLECTION_NAME') or 'Requested by' PLEX_COLLECTION_VISIBILITY_RECOMMENDED, PLEX_COLLECTION_VISIBILITY_HOME, PLEX_COLLECTION_VISIBILITY_SHARED = visibility_map.get(os.getenv('PLEX_COLLECTION_VISIBILITY').lower(), (False, False, False)) #Overseerr configuration OVERSEERR_URL = os.getenv('OVERSEERR_URL') OVERSEERR_API_KEY = os.getenv('OVERSEERR_API_KEY') OVERSEERR_PAGE_SIZE = int(os.getenv('OVERSEERR_PAGE_SIZE')) or 500 # === FUNCTIONS === def merge_lists(old_list, new_list): cleaned_old_list = [ item for item in old_list if not (item.startswith("overseerr") and item not in new_list) ] merged = list(dict.fromkeys(cleaned_old_list + new_list)) sorted_merged = [x for x in merged if not x.startswith("overseerr")] + \ [x for x in merged if x.startswith("overseerr")] return sorted_merged def parseFilter(filterString): tokens = re.split(r'([&|])', filterString) parsed_filter = [] i = 0 while i < len(tokens): cond = tokens[i].strip() sep = tokens[i + 1].strip() if i + 1 < len(tokens) else '' match = re.match(r'([^!=]+)(!?=)(.+)', cond) if match: key, operator, values_str = match.groups() values = [v.strip() for v in values_str.split(',') if v.strip()] parsed_filter.append({ 'key': key.strip(), 'operator': operator, 'values': values, 'separator': sep }) i += 2 return parsed_filter def rebuildFilter(parsed_filter): rebuilt = '' for i, cond in enumerate(parsed_filter): part = f"{cond['key']}{cond['operator']}{','.join(cond['values'])}" rebuilt += part if cond['separator']: rebuilt += cond['separator'] return rebuilt.strip() def updateUserSettings(targetUser, plexUsers): other_users = [user for user in plexUsers if user.id != targetUser.id] if myPlex.id != targetUser.id and myPlex.id not in [user.id for user in other_users]: other_users.append(myPlex) label_filter_values = [f"overseerr{user.id}" for user in other_users] filter_str = f"label!={'{}'.format(','.join(label_filter_values))}" if label_filter_values else "" print(f"Setting filter for {targetUser.username} (id: {targetUser.id}): {filter_str}") url = f"https://plex.tv/api/friends/{targetUser.id}" headers = { "X-Plex-Token": PLEX_TOKEN, "Accept": "application/json" } payload = { "server_id": plexServer.machineIdentifier, "filterMovies": filter_str, "filterTelevision": filter_str } response = requests.put(url, headers=headers, data=payload) print(f"API response: {response.status_code} {response.text}") if response.status_code == 200: print(f"Updated settings for user: {targetUser.username or targetUser.title} ({getattr(targetUser, 'email', None)})") else: print(f"Failed to update user {targetUser.username}: {response.status_code} {response.text}") def getOverseerrRequests(): all_requests = [] skip = 0 headers = { 'X-Api-Key': OVERSEERR_API_KEY } while True: params = { "take": OVERSEERR_PAGE_SIZE, "skip": skip } response = requests.get(f"{OVERSEERR_URL}/request", headers=headers, params=params) response.raise_for_status() data = response.json() results = data.get("results", []) all_requests.extend(results) print(f"Fetched {len(results)} items (skip={skip})") # Check if we've retrieved all results if len(results) < OVERSEERR_PAGE_SIZE: break skip += OVERSEERR_PAGE_SIZE print(f"Total results fetched: {len(all_requests)}") return all_requests def createOrUpdatePlexCollection(title, items, label, sectionName): section = plexServer.library.section(sectionName) existing_collections = plexServer.search(query=title, mediatype='collection') existing_collections = [col for col in existing_collections if col.librarySectionID == section.key] if existing_collections: existing_collection = existing_collections[0] existing_collection.removeItems(existing_collection.items()) existing_collection.addItems(items) existing_collection.addLabel(label, True) existing_collection.editTitle(title).editSortTitle(f"!!{title}") existing_collection.visibility().updateVisibility(PLEX_COLLECTION_VISIBILITY_RECOMMENDED, PLEX_COLLECTION_VISIBILITY_HOME, PLEX_COLLECTION_VISIBILITY_SHARED) print(f"Updated existing collection: {existing_collection.title}") else: print(f"Creating collection '{title}' for user {user.username or user.title}.") new_collection = plexServer.createCollection( title=title, items=items, section=section ) new_collection.addLabel(label, True) new_collection.editSortTitle(f"!!{title}") new_collection.visibility().updateVisibility(PLEX_COLLECTION_VISIBILITY_RECOMMENDED, PLEX_COLLECTION_VISIBILITY_HOME, PLEX_COLLECTION_VISIBILITY_SHARED) print(f"Created collection: {new_collection.title}") # === CONNECT TO PLEX === print("=== Connecting to Plex ===") plexServer = PlexServer(PLEX_URL, PLEX_TOKEN) print("Authenticating Plex account...") myPlex = MyPlexAccount(token=PLEX_TOKEN) plexUsers = myPlex.users() print(f"Authenticated. Found {len(plexUsers)} Plex user(s).") # === UPDATE USER SETTINGS === print("\n=== Updating User Settings ===") for user in plexUsers: updateUserSettings(user, plexUsers) print("User settings updated.") # === FETCH OVERSEERR REQUESTS === print("\n=== Fetching Requests from Overseerr ===") allRequests = getOverseerrRequests() print(f"Fetched {len(allRequests)} request(s).") # === ORGANIZE REQUESTS BY USER AND TYPE === print("\n=== Organizing Requests ===") requests_by_user = {} for request in allRequests: user_id = request['requestedBy']['plexId'] if user_id not in requests_by_user: requests_by_user[user_id] = {'movies': [], 'tv': []} if request['type'] == 'movie': requests_by_user[user_id]['movies'].append(request) elif request['type'] == 'tv': requests_by_user[user_id]['tv'].append(request) print(f"Organized requests for {len(requests_by_user)} user(s).") # === PROCESS COLLECTIONS FOR EACH USER === print("\n=== Creating/Updating Plex Collections ===") for user_id, requests in requests_by_user.items(): if user_id is None: print("Skipping unknown user (no Plex ID).") continue user = myPlex.user(user_id) if user_id != myPlex.id else myPlex if not user: print(f"User with ID {user_id} not found in Plex. Skipping.") continue username_display = user.username or user.title print(f"\n--- Processing user: {username_display} ({user.email}) ---") title = f"{PLEX_COLLECTION_NAME} {username_display}" label = [f"overseerr{user.id}"] # Movies if requests['movies']: print(f"Adding {len(requests['movies'])} movie(s) to collection...") rating_keys = [ int(request['media']['ratingKey']) for request in requests['movies'] if request['media']['ratingKey'] is not None ] items = plexServer.fetchItems(ekey=rating_keys) createOrUpdatePlexCollection( title=title, items=items, label=label, sectionName=PLEX_MOVIE_SECTION_NAME ) print("Movie collection updated.") # TV Shows if requests['tv']: print(f"Adding {len(requests['tv'])} TV show(s) to collection...") rating_keys = [ int(request['media']['ratingKey']) for request in requests['tv'] if request['media']['ratingKey'] is not None ] items = plexServer.fetchItems(ekey=rating_keys) createOrUpdatePlexCollection( title=title, items=items, label=label, sectionName=PLEX_TV_SECTION_NAME ) print("TV show collection updated.") print("\nAll collections have been processed.")