Writing a Plugin - SpaceK33z/web-to-plex GitHub Wiki

How to write a plugin (support for a site) for Web to Plex

This will be an in-depth, hands-on tutorial, you can skip to the end if you'd just like the overall layout.

Please be aware that the code is hidden and folds out (to make it easier to follow).

Example
// Code will be in here

What You'll Need:

  • Obviously:
    • a text editor (to make files)
    • a supported version of Web to Plex (at least v4.1)
    • a supported browser
    • a basic knowledge of JavaScript (Vanilla, a.k.a. "Ordinary")

Setup

  1. Unpackage Web to Plex, so that you have access to all of its files
  2. Navigate to the appropriate directory (folder)
    • Chrome: \src
    • Edge: \win
    • Firefox (only contains \moz): \moz
    • Opera (only contains \opa): \opa
  3. Navigate to \cloud\plugin
    • Firefox and Opera plugins must begin with plugin.
  4. Create {site-name}.js
    • For Firefox and Opera, plugin.{site-name}.js
  5. Inside of {site-name}.js (plugin.{site-name}.js for Firefox and Opera), you must create a JSON object named plugin
    let plugin = { ... };
  6. The plugin variable must contain these values:
    • The URL(s) Web to Plex should run on: "url": "<URL RegExp>"
    • The function Web to Plex will call on when ready: "init": function(<Boolean IsReady>) { ... }
  7. After that, you must give the user the option to enable the site by altering \options\index.js (\options.js for Firefox and Opera)
Example of what's been shown so far (Redbox.com for Chrome):

Files and folders

Web to Plex
>   \src
>       \cloud
>           \plugin
>               + redbox.js

Firefox/Opera

Web to Plex
>   \moz
>       + plugin.redbox.js

\options\index.js (\options.js for Firefox and Opera)

// ...
        __options__ = [
            // ...
            'plugin_redbox'
        ];

// ...
let plugins = {
    // ...
    'Redbox': ['https://redbox.com/', 'http://redbox.com/']
};

// ...

\plugins\cloud\redbox.js (plugin.redbox.js for Firefox and Opera)

let plugin = {
    "url": "https://redbox.com/",
    "init": function(ready) {
        // code to run goes here
    }
};



Getting access to the DOM (webpage)

  1. It's good practice to inspect the page and get the lowest and/or most unique selector(s) of an element/group of elements
    • Chrome, Edge and Opera: F12 OR Ctrl + Shift + I OR Cmd + Shift + I
    • Firefox: F12
  2. Once you've got the selector(s) for the element(s) you want your plugin to grab, you can add:
    let element = $("<Element Selector(s)>");
    • element is now an Array of all elements matching the selector
    • $ is a special function contained only within Web to Plex. After calling it, the Array will have these special properties/methods:
      • .first — The first element that was found
      • .last — The last element that was found
      • .child(n) — Grabs the nth child (similar to CSS' :nth-child(n))
      • .empty — A boolean (true or false) value telling you if the array is empty (true) or not empty (false)
  3. Now you can get information or manipulate the page as needed. We'll use the movie "Ad Astra (2019)" for the examples
  4. Once you're done, you have to return (give back) an Object with the following data:
    • title - the item's title/name
    • type - the item's type (movie film cinema tv show OR series)
  5. Alongside that, you can also give back these values:
    • year - the item's release year
    • image - the item's poster/thumbnail URL
    • IMDbID - the item's IMDb ID
    • TMDbID - the item's TMDb ID
    • TVDbID - the item's TVDb ID
Example of what's been shown so far (Redbox.com for Chrome):
let plugin = {
    "url": "https://redbox.com/",
    "init": function(ready) {
        let movie_title = $("[data-test-id$='-name'i]").first;
        // [data-test-id] - all elements that have the "data-test-id" attribute
        // $='-name'i - all that END with "-name" (case insensitive/ignore case)
        // $(...).first - returns the first (and only) element, instead of the whole array

        return { title: movie_title, type: "movie" };
    }
};

We can then continue, and get the rest of the data like so... (Redbox.com for Chrome)
let plugin = {
    "url": "https://redbox.com/",
    "init": function(ready) {
        let movie_title = $("[data-test-id$='-name'i]").first; // Movie's title
        let movie_year  = $("[data-test-id$='-info'i]").first; // Movie's year
        let movie_image = $("[data-test-id$='-img'i]").first;  // Movie's poster

        return { title: movie_title, type: "movie", year: movie_year, image: movie_image.src };
    }
};



Narrowing down the URL

  • Unfortunately, our plugin runs on every page, and fails on ones that don't actually have movies, but we can fix that by adjusting the URL pattern
  • The URL pattern accepts the same patterns as in the manifest (\manifest.json), but has more features (*://*.github.*/SpaceK33z/*https://github.com/SpaceK33z/wiki)
    • *:// - match any protocol (https, http, etc.)
    • *.domain - match any sub-domain (www, username, etc.)
    • domain.* - match any top level domain (com, net, etc.)
    • /* - match any path
    • ?* - match any query
    • &* - match any sub-query
    • #* - match any hash
  1. Let's adjust "url" to play better with Redbox using a move's URL as a guide. We'll use Ad Astra (https://www.redbox.com/movies/ad-astra)

  2. Make "url" similar to Ad Astra's, but more lenient (to accept all movies on the site)

    Example
    let plugin = {
        "url": "https://redbox.com/movies/*", // /* - will match "ad-astra" and anything else that isn't "/" or "*"
        "init": function(ready) {
            let movie_title = $("[data-test-id$='-name'i]").first;
            let movie_year  = $("[data-test-id$='-info'i]").first;
            let movie_image = $("[data-test-id$='-img'i]").first;
    
            return { title: movie_title, type: "movie", year: movie_year, image: movie_image.src };
        }
    };
  3. Fantastic. We've now got a plugin that can get the name, year, and image of any movie on Redbox. But, it wouldn't be of much use to have a Redbox plugin that only work on movies, we can also add TV shows

    Example
    let plugin = {
        "url": "https://redbox.com/(movies|tvshows)/*", // (movie|tvshows) - will match either one
        "init": function(ready) {
            let item_title = $("[data-test-id$='-name'i]").first; // Item's title
            let item_year  = $("[data-test-id$='-info'i]").first; // Item's year
            let item_image = $("[data-test-id$='-img'i]").first;  // Item's poster
    
            return { title: item_title, type: "movie", year: item_year, image: item_image.src };
        }
    };



Adding extra functions, methods, and properties

  1. Great. Now our plugin can get any movie's or show's information, but there's one problem: the plugin thinks everything is a movie. We can fix this though, by specifying the item's type

    Example
    let plugin = {
        "url": "https://redbox.com/(movies|tvshows)/*",
        "init": function(ready) {
            let item_title = $("[data-test-id$='-name'i]").first; // Item's title
            let item_year  = $("[data-test-id$='-info'i]").first; // Item's year
            let item_image = $("[data-test-id$='-img'i]").first;  // Item's poster
            let item_type = plugin.whatsMyType(); // Item's type
    
            return { title: item_title, type: item_type, year: item_year, image: item_image.src };
        },
    
        "whatsMyType": function() {
            if(location.pathname.startsWith("/movies"))
                return "movie";
            return "show";
        }
    };
  2. There we go. Now our plugin can correctly identify a movie or show on Redbox, without bothering to run on pages that are useless to us




Waiting for the page to load

  • Though our plugin works correctly, some users may experience slower connections and it will fail for no other reason than being slow
  1. We can add the "ready" method to plugin to specify when the page is ready for the plugin to run

    Example
    let plugin = {
        "url": "https://redbox.com/(movies|tvshows)/*",
        "ready": function() {
            // this will be the value given to plugin.init
            // though it should ALWAYS be true, it could help catch errors if given false
            let notReady = $("[data-test-id$='-name'i]").empty;
            // if empty (no item name), then obviously we're not ready
            if(notReady)
                return false;
            return true;
        },
        "init": function(ready) {
            let item_title = $("[data-test-id$='-name'i]").first;
            let item_year  = $("[data-test-id$='-info'i]").first;
            let item_image = $("[data-test-id$='-img'i]").first;
            let item_type = plugin.whatsMyType();
    
            return { title: item_title, type: item_type, year: item_year, image: item_image.src };
        },
    
        "whatsMyType": function() {
            if(location.pathname.startsWith("/movies"))
                return "movie";
            return "show";
        }
    };
  • The plugin can now wait for the page to finish loading before running



Waiting for the page to fail

  • Similarly to waiting for the page to load, we can wait for it to fail to retry (say, information hasn't finished loading)
  1. We can use the "timeout" property to tell Web to Plex how long to wait before retrying

    Example
    let plugin = {
        "url": "https://redbox.com/(movies|tvshows)/*",
        "ready": function() {
            let notReady = $("[data-test-id$='-name'i]").empty;
            if(notReady)
                return false;
            return true;
        },
        "timeout": 3000, // 3000 milliseconds = 3 seconds
        "init": function(ready) {
            let item_title = $("[data-test-id$='-name'i]").first;
            let item_year  = $("[data-test-id$='-info'i]").first;
            let item_image = $("[data-test-id$='-img'i]").first;
            let item_type = plugin.whatsMyType();
    
            return { title: item_title, type: item_type, year: item_year, image: item_image.src };
        },
    
        "whatsMyType": function() {
            if(location.pathname.startsWith("/movies"))
                return "movie";
            return "show";
        }
    };
  • Now, if the page fails to load correctly, Web to Plex will wait 3s before retrying plugin.init

  • Alternatively, you can also return a number (greater than 0)

    Example
    let plugin = {
        "url": "https://redbox.com/(movies|tvshows)/*",
        "ready": function() {
            let notReady = $("[data-test-id$='-name'i]").empty;
            if(notReady)
                return false;
            return true;
        },
        "init": function(ready) {
            let item_title = $("[data-test-id$='-name'i]").first;
            let item_year  = $("[data-test-id$='-info'i]").first;
            let item_image = $("[data-test-id$='-img'i]").first;
            let item_type = plugin.whatsMyType();
    
            if(!ready || !item_title || !item_year || !item_image || !item_type)
                return 3000;
            // come back in 3s
    
            return { title: item_title, type: item_type, year: item_year, image: item_image.src };
        },
    
        "whatsMyType": function() {
            if(location.pathname.startsWith("/movies"))
                return "movie";
            return "show";
        }
    };



Stopping the page from loading

  • If you absolutely know your plugin doesn't need to run (say, the page is known to fail), you can tell Web to Plex to stop running on the page
  1. Simply return -1 instead of an Object

    Example
    let plugin = {
        "url": "https://redbox.com/(movies|tvshows)/*",
        "ready": function() {
            let notReady = $("[data-test-id$='-name'i]").empty;
            if(notReady)
                return false;
            return true;
        },
        "timeout": 3000,
        "init": function(ready) {
            if(location.pathname.endsWith("/featured"))
                return -1;
                // No movie or show available
    
            let item_title = $("[data-test-id$='-name'i]").first;
            let item_year  = $("[data-test-id$='-info'i]").first;
            let item_image = $("[data-test-id$='-img'i]").first;
            let item_type = plugin.whatsMyType();
    
            return { title: item_title, type: item_type, year: item_year, image: item_image.src };
        },
    
        "whatsMyType": function() {
            if(location.pathname.startsWith("/movies"))
                return "movie";
            return "show";
        }
    };



Requesting special permissions

  • Some sites require login information, API keys, or other values in order to complete their jobs (such as searching for data). Your plugin can ask the user for access to these using a simple syntax
    // "Your Plugin Name" requires: <requirements>
  • Where <requirements> is a comma separated list of:
    • client clientid - the API key to Plex (also known as the "Client ID")
    • server servers - the server address(es) to Plex
    • token tokens - the user's API keys to their Managers (Radarr, Sonarr, etc.) & Plex
    • url urlroot proxy - the URLs to the user's Managers; and the user's proxy settings (URL and Headers)
    • username usernames - the user's usernames to their Managers
    • password passwords - the user's passwords to their Managers
    • storage - the user's folder locations of their Managers
    • quality qualities - the user's quality settings for their Managers
    • cache - the user's cached data: permissions, searches, etc.
    • builtin plugin - the user's enabled/disabled sites
    • api - the user's external API keys (TMDb, OMDb, etc.)
  • Once the user accepts the prompt, all related values will be available in the configuration variable, such as configuration.TMDbAPI



Making code easier to read and edit

  • There are specific semantics, syntaxes, and layouts used in Web to Plex, but there isn't one concrete one (yet)
  • The currently most used (and accepted) layout uses ES6 (2018) short-hands where possible, quoted properties/methods, and single declarations using let
Best Practices:
// "Friendly Name" requires: requirements...

let plugin = {
	"url": "< URL RegExp >",
    // URL pattern(s)

	"ready": () => true,
    // return a boolean to describe if the page is ready

	"timeout": 1000,
    // if the plugin fails to complete, retry after ... milliseconds

	"init": (ready) => {
		let title = $('#title').first,
			year  = $('#year').first,
			image = $('#image').first.src,
			type  = plugin.getType();

		return { type, title, year, image };
	},

	"getType": () => (isMovie? 'movie': 'show'),
};
⚠️ **GitHub.com Fallback** ⚠️