Verify or Request Library Analysis via Script

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.


:magnifying_glass_tilted_left: 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%.


:gear: 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


:toolbox: 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. :joy:

1 Like

You may ‘Analyze’ a TV Show by going to the TV Show library, locate the show in question and click on the ellipsis (bottom right corner of poster) and select Analyze.

On the TV Show’s synopsis page you may do the same as above on a season poster to Analyze a season.

To Analyze an entire library, go to Plex/Web Home page and move the mouse over the pinned library name you wish to analyze > Manage Library > Analyze.

yea, I know 100% about that.

I’ve been doing that for years, but the GUI doensn’t show you when they were (ever) analyzed and as far as I know, you can’t use the Plex filters to find which videos were skipped.

When you have large libraries, my TV Collection has well over 120,000 episodes, going through each episode 1-by-1 (or selecting 100 at a time) is still very time-consuming.

Not to mention, you never know if your bulk Analyzing request completed, or where it ended.

The Scripts I posted give you that information and can save you a lot of time.

The GUI can’t show you this kind of detail. This can be very helpful if you recently, or not so recently, activated a new Analysis for the library.

Analyze Media | Plex Support

The scripts I created use the PlexAPI to verify that all the files in a particular Library have been scanned, and gives you a way to only Analyze the files which have not been previously scanned or re-Analyze your Library starting with the files with the oldest data.

You don’t know when or if the task was ever completed or where it ended.

The scrips will help you find those files and can analyze them without requesting your entire library to analyze them again.

I personally don’t know the order or priority Plex uses to start and complete these tasks.

If my tool is redundant, ok. That would explain why it hasn’t been made before.

But I have found the data provided useful.

1 Like

These look like what i’m currently after, as analyzing through the web interface at the library/series levels doesn’t do anything. At the season level it does intros, but not credits. At the episode level it does everything.

I set up the scripts, but it looks like both of them are the same verify script in your first post. Would you be able to edit with the update script? Thanks.

Unfortunately, it takes many tries to get either CoPilot or ChatGPT to create a useable script with all the changes that have been requested.
I try to label them as the versions are pumped out, script 1, script 2, script 11 and so on.
I’ll see if I can track the other useful scripts.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.