noobilly 0 Posted 43 minutes ago Posted 43 minutes ago Hi everyone, Like many of you, I have run into the issue where Emby creates duplicate actor or director profiles (usually due to different metadata providers or slight spelling variations). Merging these manually through the web interface is pretty tedious, especially if an actor has multiple duplicate IDs. To fix this, I asked Google Gemini to help me build a Windows tool for merging duplicate Actor/Person profiles automatically. I have compiled it into an easy-to-use .exe file. How to use it: 1. Set up your config file Before running the tool, you need to add your server details. Open the config.json file and fill in your information exactly like this (make sure to include http://): { "EMBY_URL": "http://192.168.30.248:8096", "API_KEY": "abcdefghijklmnopqrstuvwxyz1234567" } 2. Run the tool Double-click emby_actor_merge_manually.exe. Input the name of the duplicate actor or director you want to fix and press Enter. The tool will search your server and list all profiles matching that name. Type the ID of the one you want to keep and press Enter. Press Enter one last time to confirm the merge. 3. Cleanup Once the tool finishes moving everything over to the main profile, just go to your Emby Scheduled Tasks and run a Scan Media Library. Emby will automatically delete the leftover empty ghost profiles. Source Code Lastly, here is the source code of the .exe file written in Python, in case anyone prefers to run it manually as a script. Hopefully, Emby developer will implement a similar built-in function for users to do this easily as an Admin in the future import requests import urllib.parse import json import os import sys # ========================================== # CONFIGURATION LOADER # ========================================== CONFIG_FILE = "config.json" def load_config(): if not os.path.exists(CONFIG_FILE): default_config = { "EMBY_URL": "http://YOUR_SERVER_IP:8096", "API_KEY": "YOUR_API_KEY_HERE" } with open(CONFIG_FILE, 'w') as f: json.dump(default_config, f, indent=4) print(f"[{CONFIG_FILE} not found!]") print(f"I just created a blank '{CONFIG_FILE}' in this folder.") print("Please open it, paste your Emby URL and API Key, save it, and run this script again.") sys.exit() with open(CONFIG_FILE, 'r') as f: try: config = json.load(f) return config.get("EMBY_URL", "").strip('/'), config.get("API_KEY", "") except json.JSONDecodeError: print(f"Error: {CONFIG_FILE} is not formatted properly.") sys.exit() EMBY_URL, API_KEY = load_config() if not EMBY_URL or not API_KEY or "YOUR_API_KEY_HERE" in API_KEY: print(f"Error: Please add your actual Emby URL and API Key to {CONFIG_FILE}.") sys.exit() # ========================================== headers = { "X-Emby-Token": API_KEY, "Content-Type": "application/json", "Accept": "application/json" } def main(): search_name = input("Enter the name of the actor/director to search for: ").strip() if not search_name: print("Name cannot be empty. Exiting...") return print("\nAuthenticating and locating Admin User ID...") res_users = requests.get(f"{EMBY_URL}/emby/Users", headers=headers) if res_users.status_code != 200: print(f"Error fetching users. Status: {res_users.status_code}") return users = res_users.json() admin_id = None for u in users: if u.get("Policy", {}).get("IsAdministrator"): admin_id = u["Id"] break if not admin_id: print("Could not find an Admin user. Falling back to the first user.") admin_id = users[0]["Id"] print(f" -> Success! Using Admin ID: {admin_id}") print(f"\nSearching for profiles matching '{search_name}'...") safe_search_name = urllib.parse.quote(search_name) search_url = f"{EMBY_URL}/emby/Users/{admin_id}/Items?SearchTerm={safe_search_name}&IncludeItemTypes=Person&Recursive=true" res_search = requests.get(search_url, headers=headers) if res_search.status_code != 200: print(f"Error searching for actor. Status: {res_search.status_code}") return items = res_search.json().get("Items", []) if not items: print(f"No profiles found matching '{search_name}'. Exiting...") return found_profiles = {} for item in items: person_id = item.get("Id") if person_id: found_profiles[person_id] = item.get("Name", "Unknown") if len(found_profiles) == 1: print(f"\nOnly 1 profile found for '{search_name}'. No duplicates exist! Exiting...") return print(f"\nFound {len(found_profiles)} matching profiles:\n") print("=======================================================") for pid, pname in found_profiles.items(): print(f" ID: {pid} | Name: {pname}") print("=======================================================\n") keep_id = input("Enter the ID of the profile you want to KEEP: ").strip() if keep_id not in found_profiles: print(f"Error: '{keep_id}' is not in the list of found profiles. Exiting...") return keep_name = found_profiles[keep_id] duplicates_to_merge = [pid for pid in found_profiles.keys() if pid != keep_id] print(f"\n[Settings Locked]") print(f" -> KEEPING: {keep_name} ({keep_id})") print(f" -> MERGING: {len(duplicates_to_merge)} duplicate(s) into the kept profile.") confirm = input("Press ENTER to begin the merge, or type 'Q' to quit: ") if confirm.strip().lower() == 'q': print("Canceled. Exiting...") return for duplicate_id in duplicates_to_merge: duplicate_name = found_profiles[duplicate_id] print(f"\n=======================================================") print(f" Hunting down media for: {duplicate_name} (ID: {duplicate_id})") print(f"=======================================================") dup_search_url = f"{EMBY_URL}/emby/Users/{admin_id}/Items?PersonIds={duplicate_id}&Recursive=true&Fields=People" res_dup = requests.get(dup_search_url, headers=headers) if res_dup.status_code != 200: print(f"Error fetching items for {duplicate_id}. Status: {res_dup.status_code}") continue dup_items = res_dup.json().get("Items", []) print(f"Found {len(dup_items)} items containing this duplicate.") if not dup_items: print("No merging needed. This duplicate has no attached media.") continue for item in dup_items: item_id = item["Id"] item_name = item.get("Name", "Unknown") print(f"\nProcessing: {item_name} (ID: {item_id})") item_get_url = f"{EMBY_URL}/emby/Users/{admin_id}/Items/{item_id}" res_item = requests.get(item_get_url, headers=headers) if res_item.status_code != 200: print(f" -> Failed to fetch full metadata. Error {res_item.status_code}") continue full_item = res_item.json() people = full_item.get("People", []) keep_exists = any(str(p.get("Id")) == str(keep_id) for p in people) new_people = [] changed = False for p in people: if str(p.get("Id")) == str(duplicate_id): if keep_exists: print(f" -> Dropping duplicate (Main person already exists here).") changed = True continue else: print(f" -> Replacing duplicate with '{keep_name}'...") p["Id"] = keep_id p["Name"] = keep_name changed = True new_people.append(p) if changed: full_item["People"] = new_people update_url = f"{EMBY_URL}/emby/Items/{item_id}" update_res = requests.post(update_url, headers=headers, json=full_item) if update_res.status_code in [200, 204]: print(f" -> Successfully updated metadata!") else: print(f" -> Failed to update. Error {update_res.status_code}") else: print(" -> No changes needed.") print("\nMerge complete! Run 'Scan Media Library' in Emby to wipe the leftover duplicate profiles.") if __name__ == "__main__": main() emby_actor_merge_manually.exe config.json
Abobader 3504 Posted 43 minutes ago Posted 43 minutes ago Hello noobilly, ** This is an auto reply ** Please wait for someone from staff support or our members to reply to you. It's recommended to provide more info, as it explain in this thread: Thank you. Emby Team
Recommended Posts
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now