Server Version#: Any
Player Version#: n/a
I have several large libraries, and even though I’ve configured them to:
- Analyze new media as it’s added
- Perform partial scans when changes are detected
- Scan every 6 hours
…I still regularly come across series episodes or seasons that are missing preview thumbnails or haven’t been analyzed at all.
With large libraries, manually analyzing content is a chore—especially since (as far as I know) you can’t analyze by series or season, only per episode.
So, I worked with Copilot (Microsoft’s AI assistant) to create a couple of Python scripts using the PlexAPI package. These scripts help identify which files have or haven’t been analyzed, and optionally trigger analysis in bulk—just like clicking “Analyze” in the web GUI, but automated.
What the Scripts Do
- Script 1: Scans your libraries and reports which files have never been analyzed or when they were last analyzed. This is especially useful if you’ve recently changed your library’s indexing settings.
- Script 2: Lets you choose how many files to analyze, then sends the appropriate API requests to Plex. It mimics the manual Analyze function but at scale.
To avoid overloading your system, the script monitors CPU usage and pauses for 10 seconds at a time if utilization exceeds 65%.
Setup Requirements
You’ll need to edit the script with your Plex token and server IP:
# Plex server details – update these with your server’s info
PLEX_URL = "http://127.0.0.1:32400"
PLEX_TOKEN = "YOUR_PLEX_TOKEN"
Plex token instructions: Finding your X-Plex-Token
Python Environment
To run the scripts, install Python and the required package:
pip install plexapi
Make sure the device running the script can reach your Plex server (locally or remotely). If you’re using Docker or a VM, ensure port 32400
is accessible.
I’m not super technical when it comes to Python setup (I’m on Windows), but Copilot was a huge help—feel free to ask it for guidance if you get stuck. Happy to share the scripts if folks are interested or want to collaborate on improvements.
Verify Indexing Script for Plex
import time
import psutil
import json
from plexapi.server import PlexServer
from datetime import datetime
# Plex server details – update with your server info
PLEX_URL = "http://127.0.0.1:32400"
PLEX_TOKEN = "PLEX_TOKEN = "YOUR_PLEX_TOKEN""
# Connect to Plex
try:
plex = PlexServer(PLEX_URL, PLEX_TOKEN, timeout=60)
except Exception as e:
print(f"Error connecting to Plex: {e}")
exit()
LOG_FILE = "index_update_log.json"
def load_status():
"""Loads the JSON log that records index updates to avoid repetition."""
try:
with open(LOG_FILE, "r") as f:
return json.load(f)
except FileNotFoundError:
return {}
def save_status(status_data):
"""Saves the current index update logging data."""
with open(LOG_FILE, "w") as f:
json.dump(status_data, f, indent=4)
def get_index_data(library):
"""
Retrieves media items along with title, file path, last update time,
and (if available) the year. Also includes a flag for index update.
"""
media_items = library.search()
if not media_items:
return {}
index_data = {}
for item in media_items:
if item.updatedAt:
try:
file_path = item.media[0].parts[0].file
except (AttributeError, IndexError):
file_path = "Unknown"
index_data[item.ratingKey] = {
"title": item.title,
"file_path": file_path,
"updatedAt": item.updatedAt,
"year": getattr(item, "year", "n/a"),
# Adjust this check as per your logic on whether a file is updated.
"has_updated_index": "preview" in item.__dict__ and bool(item.preview)
}
return index_data
def show_index_breakdown(library):
"""
Displays a breakdown by the last index update date as well as the pending
count (pending = files missing updated index info). If the pending count
equals the total number of files, it notes that all files are already
marked by Plex as being indexed.
"""
index_data = get_index_data(library)
if not index_data:
print(f"\nNo media items found in {library.title}.")
return {}, 0
now = datetime.now()
breakdown = {
"Older than 2 years": 0,
"Older than 1 year": 0,
"Older than 6 months": 0,
"Within 6 months": 0
}
total_files = len(index_data)
pending_count = 0 # count files that need an index update
for data in index_data.values():
age_days = (now - data["updatedAt"]).days
if age_days > 730:
breakdown["Older than 2 years"] += 1
elif age_days > 365:
breakdown["Older than 1 year"] += 1
elif age_days > 180:
breakdown["Older than 6 months"] += 1
else:
breakdown["Within 6 months"] += 1
if not data.get("has_updated_index", False):
pending_count += 1
print("\nIndex Breakdown (by last update date):")
for category, count in breakdown.items():
print(f" {category}: {count} file(s)")
print(f"Pending index update files: {pending_count} file(s)")
if pending_count == total_files:
print("\nNote: Plex has already marked all files as being indexed.")
return breakdown, pending_count
def update_missing_index(library, num_files):
"""
Processes media items missing updated index settings (e.g. preview thumbnails)
and logs the update in a JSON file.
"""
status_data = load_status()
index_data = get_index_data(library)
# Identify items that need an update and haven't been processed already.
to_update = {
k: v for k, v in index_data.items()
if not v.get("has_updated_index", False) and k not in status_data
}
if not to_update:
print("\nNo items require an index update!")
return
sorted_items = sorted(to_update.items(), key=lambda x: x[1]["updatedAt"])[:num_files]
print(f"\nUpdating index settings for {len(sorted_items)} file(s) – log saved in '{LOG_FILE}'")
for key, data in sorted_items:
while psutil.cpu_percent(interval=1) > 65:
print(f"High CPU usage detected ({psutil.cpu_percent()}%). Pausing...")
time.sleep(10)
print(f"{data['updatedAt'].strftime('%Y-%m-%d')} - {data['title']} ({data['year']}) -- {data['file_path']} (Updating index...)")
try:
plex.fetchItem(key).analyze() # Or use a different method if needed.
except Exception as ex:
print(f"Error updating index for {data['title']}: {ex}")
continue
status_data[key] = {
"title": data["title"],
"file_path": data["file_path"],
"updatedAt": data["updatedAt"].strftime("%Y-%m-%d"),
"update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
save_status(status_data)
print("\nIndex update complete!")
def choose_library():
"""Lists libraries available to the user and returns the selected one or None."""
libraries = plex.library.sections()
print("\nAvailable Libraries:")
for i, lib in enumerate(libraries, 1):
print(f" {i}. {lib.title} ({lib.type})")
print(" 0. Return to main menu")
choice = input("\nEnter the number of the library (or 0 to return): ").strip()
if choice == "0":
return None
try:
return libraries[int(choice) - 1]
except (ValueError, IndexError):
print("Invalid choice. Returning to main menu.")
return None
def main_menu():
"""Main menu loop for the index update script."""
print("Note: Index update requests are logged in a JSON file named 'index_update_log.json'.")
while True:
print("\nOptions:")
print("1. Update missing index settings")
print(" (Displays a breakdown so you know which files require an update.)")
print("2. Show index breakdown")
print(" (Displays counts of files by last index update date.)")
print("3. Show details for the oldest indexed file")
print(" (Displays one line with date, title, year, and file path.)")
print("4. Exit")
choice = input("\nEnter your choice: ").strip()
if choice == "4":
print("\nExiting script. Goodbye!")
break
if choice in ["1", "2", "3"]:
lib = choose_library()
if lib is None:
continue
if choice == "1":
_, pending = show_index_breakdown(lib)
action = input("\nEnter 'c' to continue with index update or 'r' to return: ").strip().lower()
if action != "c":
continue
try:
num_files = int(input("\nEnter the number of files to update (as suggested by the breakdown): ").strip())
except ValueError:
print("Invalid number. Returning to main menu.")
continue
update_missing_index(lib, num_files)
elif choice == "2":
show_index_breakdown(lib)
elif choice == "3":
index_data = get_index_data(lib)
if not index_data:
print(f"\nNo media items found in {lib.title}.")
continue
oldest = min(index_data.values(), key=lambda x: x["updatedAt"])
print(f"\nOldest indexed file: {oldest['updatedAt'].strftime('%Y-%m-%d')} - {oldest['title']} ({oldest.get('year','n/a')}) -- {oldest['file_path']}")
else:
print("Invalid option. Please try again.")
if __name__ == "__main__":
print("Starting Plex Re-Analyze Script...")
main_menu()
# Added note about Plex metadata refresh behavior:
print("\nNote: Plex may not update the updatedAt metadata immediately. "
"Changes made by this script might not be reflected until Plex’s next metadata refresh or optimization cycle.")
input("\nPress Enter to exit...")
main_menu()
input("\nPress Enter to exit...")
Index Update Script for Plex
import time
import psutil
import json
from plexapi.server import PlexServer
from datetime import datetime
# Plex server details – update these with your server’s info
PLEX_URL = "http://127.0.0.1:32400"
PLEX_TOKEN = "YOUR_PLEX_TOKEN"
# Connect to Plex
try:
plex = PlexServer(PLEX_URL, PLEX_TOKEN, timeout=60)
except Exception as e:
print(f"Error connecting to Plex: {e}")
exit()
LOG_FILE = "index_update_log.json"
def load_status():
"""Loads the JSON log that records index updates to avoid repetition."""
try:
with open(LOG_FILE, "r") as f:
return json.load(f)
except FileNotFoundError:
return {}
def save_status(status_data):
"""Saves the current index update logging data."""
with open(LOG_FILE, "w") as f:
json.dump(status_data, f, indent=4)
def get_index_data(library):
"""
Retrieves media items along with title, file path, last update time,
and (if available) the year. Also includes a flag for index update.
"""
media_items = library.search()
if not media_items:
return {}
index_data = {}
for item in media_items:
if item.updatedAt:
try:
file_path = item.media[0].parts[0].file
except (AttributeError, IndexError):
file_path = "Unknown"
index_data[item.ratingKey] = {
"title": item.title,
"file_path": file_path,
"updatedAt": item.updatedAt,
"year": getattr(item, "year", "n/a"),
# Adjust this check as per your logic on whether a file is updated.
"has_updated_index": "preview" in item.__dict__ and bool(item.preview)
}
return index_data
def show_index_breakdown(library):
"""
Displays a breakdown by the last index update date as well as the pending
count (pending = files missing updated index info). If the pending count
equals the total number of files, it notes that all files are already
marked by Plex as being indexed.
"""
index_data = get_index_data(library)
if not index_data:
print(f"\nNo media items found in {library.title}.")
return {}, 0
now = datetime.now()
breakdown = {
"Older than 2 years": 0,
"Older than 1 year": 0,
"Older than 6 months": 0,
"Within 6 months": 0
}
total_files = len(index_data)
pending_count = 0 # count files that need an index update
for data in index_data.values():
age_days = (now - data["updatedAt"]).days
if age_days > 730:
breakdown["Older than 2 years"] += 1
elif age_days > 365:
breakdown["Older than 1 year"] += 1
elif age_days > 180:
breakdown["Older than 6 months"] += 1
else:
breakdown["Within 6 months"] += 1
if not data.get("has_updated_index", False):
pending_count += 1
print("\nIndex Breakdown (by last update date):")
for category, count in breakdown.items():
print(f" {category}: {count} file(s)")
print(f"Pending index update files: {pending_count} file(s)")
if pending_count == total_files:
print("\nNote: Plex has already marked all files as being indexed.")
return breakdown, pending_count
def update_missing_index(library, num_files):
"""
Processes media items missing updated index settings (e.g. preview thumbnails)
and logs the update in a JSON file.
"""
status_data = load_status()
index_data = get_index_data(library)
# Identify items that need an update and haven't been processed already.
to_update = {
k: v for k, v in index_data.items()
if not v.get("has_updated_index", False) and k not in status_data
}
if not to_update:
print("\nNo items require an index update!")
return
# Sort items by their updatedAt timestamp (earliest first)
sorted_items = sorted(to_update.items(), key=lambda x: x[1]["updatedAt"])[:num_files]
print(f"\nUpdating index settings for {len(sorted_items)} file(s) – log saved in '{LOG_FILE}'")
for key, data in sorted_items:
while psutil.cpu_percent(interval=1) > 65:
print(f"High CPU usage detected ({psutil.cpu_percent()}%). Pausing...")
time.sleep(10)
print(f"{data['updatedAt'].strftime('%Y-%m-%d')} - {data['title']} ({data['year']}) -- {data['file_path']} (Updating index...)")
try:
plex.fetchItem(key).analyze() # Or use the appropriate method if necessary.
except Exception as ex:
print(f"Error updating index for {data['title']}: {ex}")
continue
status_data[key] = {
"title": data["title"],
"file_path": data["file_path"],
"updatedAt": data["updatedAt"].strftime("%Y-%m-%d"),
"update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
save_status(status_data)
print("\nIndex update complete!")
def choose_library():
"""Lists available libraries for user selection and returns the chosen library or None."""
libraries = plex.library.sections()
print("\nAvailable Libraries:")
for i, lib in enumerate(libraries, 1):
print(f" {i}. {lib.title} ({lib.type})")
print(" 0. Return to main menu")
choice = input("\nEnter the number of the library (or 0 to return): ").strip()
if choice == "0":
return None
try:
return libraries[int(choice) - 1]
except (ValueError, IndexError):
print("Invalid choice. Returning to main menu.")
return None
def main_menu():
"""Main menu loop for the index update script."""
print("Note: Index update requests are logged in a JSON file named 'index_update_log.json'.")
while True:
print("\nOptions:")
print("1. Update missing index settings")
print(" (Displays a breakdown so you know which files require an update.)")
print("2. Show index breakdown")
print(" (Displays counts of files by last index update date.)")
print("3. Show details for the oldest indexed file")
print(" (Displays one line with date, title, year, and file path.)")
print("4. Exit")
choice = input("\nEnter your choice: ").strip()
if choice == "4":
print("\nExiting script. Goodbye!")
break
if choice in ["1", "2", "3"]:
lib = choose_library()
if lib is None:
continue
# Added processing notice message before doing heavy querying.
print("\nProcessing library... please be patient while retrieving the index breakdown data.")
if choice == "1":
_, pending = show_index_breakdown(lib)
action = input("\nEnter 'c' to continue with index update or 'r' to return: ").strip().lower()
if action != "c":
continue
try:
num_files = int(input("\nEnter the number of files to update (as suggested by the breakdown): ").strip())
except ValueError:
print("Invalid number. Returning to main menu.")
continue
update_missing_index(lib, num_files)
elif choice == "2":
show_index_breakdown(lib)
elif choice == "3":
index_data = get_index_data(lib)
if not index_data:
print(f"\nNo media items found in {lib.title}.")
continue
oldest = min(index_data.values(), key=lambda x: x["updatedAt"])
print(f"\nOldest indexed file: {oldest['updatedAt'].strftime('%Y-%m-%d')} - {oldest['title']} ({oldest.get('year','n/a')}) -- {oldest['file_path']}")
else:
print("Invalid option. Please try again.")
if __name__ == "__main__":
print("Starting Plex Index Update Script...")
main_menu()
# Final note before exit.
print("\nNote: Plex may not update the updatedAt metadata immediately. Changes made by this script might not be reflected until Plex's next metadata refresh or optimization cycle.")
input("\nPress Enter to exit...")
I’m not especially technical—I just had an idea and worked with Copilot to turn it into something usable. If others want to take these scripts further, improve the logic, or expand on the functionality, feel free to update this thread with your versions. I’d love to see what smarter folks come up with.
And if you’re curious about other ways to make Plex easier to manage, try throwing some ideas at an AI like Copilot—there may be other Plex API tricks that help streamline your workflow more than you’d expect – and don’t forget to upload them so others can benefit.
*yes, I did have CoPilot clean up my writing to make it more clear.