Just as the title implies, this tutorial is going to cover what it takes to automatically cleanup a Plex media server, using Google Scripts. I am aware there are other ways to do this, but this tutorial is tailored specifically for Google Scripts.

Table of contents

Setup

Head over to the Google Script dashboard and create a new project. Before we start writing any piece of code we need some prerequisites, for our project.

Open the View menu and click on Show manifest file. This will allow us to add dependencies and other scripts to our project. Now you can open the appscript.json file and add this line before the last }.

"oauthScopes": ["https://www.googleapis.com/auth/script.external_request"]

This line gives us the ability to make external requests from our script to the Plex media server.

After adding the line above, open the File menu and click on Project properties and open the Script properties tab. You will need to add multiple key/value properties.

  1. TOKEN
  2. BASE_URL
  3. IFTTT_WEBHOOK

The last one is optional and we will cover it in the last point of this tutorial.

I'll start with number 2, which represents the Plex media server base URL. The server needs to be accessible via the Internet or else our script won't work at all. For point number 1 you'll need to do a bit of digging.

In Plex, open the page of any piece of media (this represents the last child of that media, whether it's a show or a movie). Next to the Edit button you should see three dots. Clicking on those should open a menu like in the bellow picture.

Click on Get Info and then in the modal that pops up, in the bottom left click on View XML. This will open the media metadata in the form of XML, in a new tab. From the URL of that tab you can extract the X-Plex-Token which coincidentally is the last parameter in the URL.

After we have points 1 and 2 we can start writing the actual code, for our script.

Code

I'll paste the code in it's entirety and then we can go over it line by line.

// Gets the script properties
const script = PropertiesService.getScriptProperties(),
      baseURL = script.getProperty("BASE_URL"),
      token = script.getProperty("TOKEN"),
      webhook = script.getProperty("IFTTT_WEBHOOK"),
      section = 4;

// Main constructor method
function constructor() {
  let response = UrlFetchApp.fetch(
      `${baseURL}/library/sections/${section}/allLeaves`,
      { method: "GET", headers: { "X-Plex-Token": token } }
    ),
    jsonObject = xmlToJson(response.getContentText()),
    videoContainer = jsonObject.MediaContainer.Video;

  if (videoContainer) {
    let watchedVideos = [];

    if (Array.isArray(videoContainer)) {
      watchedVideos = videoContainer.filter((video) => video.viewCount == 1);
    } else {
      if (videoContainer.viewCount == 1)    watchedVideos.push(videoContainer);
    }

    if (watchedVideos.length > 0) {
      let watchedTitles = "";

      watchedVideos.forEach((video, index) => {
        Logger.log(video.key);

        UrlFetchApp.fetch(baseURL + video.key, {
          method: "DELETE",
          headers: { "X-Plex-Token": token },
        });
          
        // Notifications
        watchedTitles += `${video.grandparentTitle} - ${video.title}`;
        watchedTitles += "\r\n";

        if (index === watchedVideos.length - 1) {
          Logger.log(watchedTitles);
          Logger.log("Notification sent!");

          let notification =
            `${watchedVideos.length} watched episodes were removed!` +
            "\r\n" +
            watchedTitles;

          let options = {
            method: "POST",
            contentType: "application/json",
            payload: JSON.stringify({ value1: notification }),
          };
          UrlFetchApp.fetch(webhook, options);
        }
      });
    }
  } else {
    Logger.log("No videos to delete!");
  }
}

This first things we need to get are the script properties we setup in the first step, of this tutorial.

Notice there is a variable called section. That represents the ID of the library in the Plex media server. You can get that, by opening the desired library you want to clean and from the URL get the value from the source parameter (hint: it should be the last one).

Next up, we start building the actual function that does the cleaning up.

Using the UrlFetchApp library and the fetch method we call the Plex media server to retrieve all the contents of our library. We parse the XML response to JSON using a method that you can find here. You can add the parsing method to the same file, if you want.

If the videoContainer object was correctly parsed we need to check its type, because Plex will return an object if there is only one child in the library, instead of an array of objects. We're going to check for the viewCount if it's true and add everything we find to an array of objects.

While iterating through the whole array, we make a DELETE type call to the Plex media server, passing the TOKEN, in the headers, and using the baseURL and videoKey as the URL for our call.

That's it. You can run the script by clicking the Run icon and the checking the logs in the View menu. You should see a list of all the items deleted.

Execution

We can automate this script to run at certain times in the day. We can do this by setting a trigger.

In order to set a trigger, open the Edit menu and click Current project's triggers and add a new trigger. Bellow is an example of how you could set it up.

There's no need to run it too often if you don't watch a lot of shows.

Notifications

This step is optional, so you don't have to follow it. I wanted a way to get notified if the script ran and what items were deleted from Plex.

I decided I was going to use IFTTT but you can use Zapier, Automate.io or even one of Google services to send an email.

The code is rather simple for IFTTT. You can find it bellow

// Notifications
watchedTitles += `${video.grandparentTitle} - ${video.title}`;
watchedTitles += "\r\n";

if (index === watchedVideos.length - 1) {
    Logger.log(watchedTitles);
    Logger.log("Notification sent!");

    let notification =
        `${watchedVideos.length} watched episodes were removed!` +
        "\r\n" +
        watchedTitles;

    let options = {
        method: "POST",
        contentType: "application/json",
        payload: JSON.stringify({ value1: notification }),
    };
    UrlFetchApp.fetch(webhook, options);
}

Add this code inside the iteration (after the DELETE call. It's already in the code I shared above). This will create an array of media titles and send them to a webhook in IFTTT that has a recipe set up to send a notification to my phone. You can customise it however you like.

Some points before I end this tutorial:

  1. This project is running on the new Apps Script runtime powered by Chrome V8
  2. If you have any ideas on how to improve the script let me know.

That's it. You can also find the code here and if you have any questions let me know. You can reach me on most social media channels.