Writing a Plugin - SpaceK33z/web-to-plex GitHub Wiki
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")
- Unpackage Web to Plex, so that you have access to all of its files
- Navigate to the appropriate directory (folder)
- Chrome:
\src
- Edge:
\win
- Firefox (only contains
\moz
):\moz
- Opera (only contains
\opa
):\opa
- Chrome:
- Navigate to
\cloud\plugin
- Firefox and Opera plugins must begin with
plugin.
- Firefox and Opera plugins must begin with
- Create
{site-name}.js
- For Firefox and Opera,
plugin.{site-name}.js
- For Firefox and Opera,
- Inside of
{site-name}.js
(plugin.{site-name}.js
for Firefox and Opera), you must create a JSON object namedplugin
let plugin = { ... };
- 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>) { ... }
- The URL(s) Web to Plex should run on:
- 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
}
};
- 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
ORCtrl + Shift + I
ORCmd + Shift + I
- Firefox:
F12
- Chrome, Edge and Opera:
- 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
)
-
-
- Now you can get information or manipulate the page as needed. We'll use the movie "Ad Astra (2019)" for the examples
- 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
ORseries
)
-
- 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 };
}
};
- 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
-
-
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
) -
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 }; } };
-
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 }; } };
-
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"; } };
-
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
- Though our plugin works correctly, some users may experience slower connections and it will fail for no other reason than being slow
-
We can add the
"ready"
method toplugin
to specify when the page is ready for the plugin to runExample
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
- Similarly to waiting for the page to load, we can wait for it to fail to retry (say, information hasn't finished loading)
-
We can use the
"timeout"
property to tell Web to Plex how long to wait before retryingExample
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"; } };
- 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
-
Simply return
-1
instead of an ObjectExample
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"; } };
- 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 asconfiguration.TMDbAPI
- 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'),
};