diff --git a/README.md b/README.md index a710d87..58a8635 100644 --- a/README.md +++ b/README.md @@ -8,49 +8,63 @@ This browser extension searches your [Plex Media Server (PMS)](https://www.plex. ---- -## Features: - -- Can save media directly from noted sites (file downloads/magnet URLs) - - Right-click | Web to Plex | Save as "Show/Movie (Year)" -- Can push requests to your chosen download manager - - [Radarr](https://radarr.video/) - - [Sonarr](https://sonarr.tv/) - - [CouchPotato](https://couchpota.to/) - - [Watcher 3](https://nosmokingbandit.github.io/) - - [Ombi](https://ombi.io/) -- Offers search options via right-click (context menu) - - Right-click | Web to Plex | Find "Show/Movie (Year)" -- Offers a Plex-like GUI - - Web to Plex button - - Settings page - - Pop-up page -- Offers a status via the browser badge and button - - Orange/Yellow: item is on Plex - - Blue (button): item isn't on Plex, but can be sent for - - Grey (badge)/Red (button): item is unavailable/not found - - Grey (button): item is loading -- Offers an easy login feature - - Offers an API login feature -- Offers a "Direct Plex URL" feature - - i.e. you can specify `localhost:32400` as your Plex URL to avoid bandwidth usage for Plex requests - -# Download Managers - -Optionally, you can configure your download manager(s) (see support table) in the extension's options. After that, you can immediately add a TV show or movie with one click, right from your favorite site. +# NZB Managers + +Optionally, you can configure NZB Manager (see support table) in the extension's options. After that, you can immediately add a TV show or movie with one click, right from your favorite site. ## Supported Managers | Manager | Movie Support | TV Show Support | Searchable -| ----------------------------------------------- | ------------- | --------------- | ---------- -| [Watcher 3](https://nosmokingbandit.github.io/) | Yes | | -| [CouchPotato](https://couchpota.to/) | Yes | Yes | -| [Radarr](https://radarr.video/) | Yes | | Yes -| [Sonarr](https://sonarr.tv/) | | Yes | Yes -| [Ombi](https://ombi.io/) | Yes | Yes | Yes +| ----------------------------------------------- |:-------------:|:---------------:|:----------: +| [Watcher 3](https://nosmokingbandit.github.io/) | ✔ | ❌ | ❌ +| [CouchPotato](https://couchpota.to/) | ✔ | ✔ | ❌ +| [Radarr](https://radarr.video/) | ✔ | ❌ | ✔ +| [Sonarr](https://sonarr.tv/) | ❌ | ✔ | ✔ +| [Ombi](https://ombi.io/) | ❔ | ❔ | ✔ +| [Medusa](https://pymedusa.com/) | ❌ | ✔ | ✔ +| [Sick Beard](https://sickbeard.com/) | ❌ | ✔ | ❌ + +### Key + +| ✔ | ❌ | ❔ | +| - | - | - | +| yes | no | yes (with help) | + +---- + +## Features +### Easy login +You can log into Plex using either an access token, your credentials, or Ombi (if setup). + +### Download (![download icon](https://github.com/SpaceK33z/web-to-plex/blob/master/src/img/16.png)) +On certain sites (denoted with the "download" icon), the user can choose to save/engage media directly, instead of waiting for their NZB manager to find the item. + +### Plex It! (![plex it icon](https://github.com/SpaceK33z/web-to-plex/blob/master/src/img/plexit.16.png)) +Click the icon to open **Plex It!** (left sidebar), click it again to add the current item(s) to your list. + +It's primary purpose is to provide a watchlist service on sites that don't support watchlists. -If you don't feel like actually downloading the movie, or want a simple watchlist, you can also use the built-in "Plex It!" feature to bookmark the current page. +### Hide Web to Plex (![hide icon](https://github.com/SpaceK33z/web-to-plex/blob/master/src/img/hide.16.png)) +Use this to hide the **Web to Plex** button. It changes the button's opacity to 10% to make it almost invisible so that it isn't as distracting on sites like Netflix. + +### Reload Web to Plex (![reload icon](https://github.com/SpaceK33z/web-to-plex/blob/master/src/img/reload.16.png)) +Use this to reload **Web to Plex** on the current page. This can sometimes fix loading issues or cache errors. + +### Plex URL +This is a *moderately advance* setting, but is very useful to know. If you know your Plex server's URL (e.g. `https://localhost:32400`), then you can specify this and avoid bandwidth usage, as the extension will communicate with Plex on your device instead of `https://app.plex.tv/`. + +### Find this | Right Click +If you aren't satisfied with a found item, or it is incorrect, you can right click the page and use the **Web to Plex | Find "XYZ"** feature to search for the item. + +The sites used as search engines (IMDb, TMDb, and TVDb) will automatically create a cached version of the information (for "Local Search" results). + +-------- ## Supported sites +
+
+ Sites + 1. [Movieo](http://movieo.me/) 2. [IMDb](http://imdb.com/) 3. [Trakt.tv](https://trakt.tv/) @@ -76,13 +90,22 @@ If you don't feel like actually downloading the movie, or want a simple watchlis 23. [Toloka](https://toloka.to/)6 24. [Shana Project](https://www.shanaproject.com/)6 25. [My Anime List](https://myanimelist.com/)6 -26. [YouTube](https://youtube.com/) -27. [Flickmetrix (Cinesift)](https://flickmetrix.com/) -28. [Allociné](https://www.allocine.fr/) -29. [MovieMeter](https://www.moviemeter.nl/) -30. [JustWatch](https://justwatch.com/) - -*Notes* +26. [My Shows](https://en.myshows.me/)6 +27. [YouTube](https://youtube.com/) +28. [Flickmetrix (Cinesift)](https://flickmetrix.com/) +29. [Allociné](https://www.allocine.fr/) +30. [MovieMeter](https://www.moviemeter.nl/) +31. [JustWatch](https://justwatch.com/) +32. [Vumoo](https://vumoo.to/)1 +33. [Web to Plex](https://ephellon.github.io/web.to.plex/)2/3/4/5 +34. [Indomovie](https://indomovie.club)5/6 +35. [Kitsu](https://kitsu.io/)6 +36. [Redbox](https://redbox.com/)6 + +
+ +
+ Key - `1` *This site uses the `Right click | Web to Plex | Save as...` feature (file download), or a direct link (usually magnet URL).* - `2` *This site has a loading issue, simply refresh the page if the button doesn't appear.* @@ -91,13 +114,17 @@ If you don't feel like actually downloading the movie, or want a simple watchlis - `5` *This site is known to update frequently, support may change without notice.* - `6` *This site is only supported via a plugin (enabled in the settings)* +
+ +
+ ## Installing Web to Plex **Download on [Chrome Webstore](https://chrome.google.com/webstore/detail/movieo-to-plex/kmcinnefmnkfnmnmijfmbiaflncfifcn).** **Download on [FireFox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/web-to-plex/).** -**Download the [SRC](https://github.com/Ephellon/web-to-plex/archive/master.zip)** +**Download the source code for [Google Chrome/Opera](src.zip), or [Firefox](moz.zip).** ## Requirements @@ -107,6 +134,6 @@ If you don't feel like actually downloading the movie, or want a simple watchlis ## Issues & Contributions -If you have any problem with the extension, please don't hesitate to [submit an issue](https://github.com/SpaceK33z/web-to-plex/issues/new). +If you have any problem with the extension, please don't hesitate to [submit an issue](https://github.com/SpaceK33z/web-to-plex/issues/new/choose). All contributions are welcome. diff --git a/moz.xpi b/moz.xpi new file mode 100644 index 0000000..5d534a8 Binary files /dev/null and b/moz.xpi differ diff --git a/moz.zip b/moz.zip new file mode 100644 index 0000000..d866934 Binary files /dev/null and b/moz.zip differ diff --git a/moz/$$$16.png b/moz/$$$16.png new file mode 100644 index 0000000..fe328e0 Binary files /dev/null and b/moz/$$$16.png differ diff --git a/moz/$$$32.png b/moz/$$$32.png new file mode 100644 index 0000000..1a38cb2 Binary files /dev/null and b/moz/$$$32.png differ diff --git a/moz/$$$48.png b/moz/$$$48.png new file mode 100644 index 0000000..76b479a Binary files /dev/null and b/moz/$$$48.png differ diff --git a/moz/$$16.png b/moz/$$16.png new file mode 100644 index 0000000..489099c Binary files /dev/null and b/moz/$$16.png differ diff --git a/moz/$$32.png b/moz/$$32.png new file mode 100644 index 0000000..6bb8609 Binary files /dev/null and b/moz/$$32.png differ diff --git a/moz/$$48.png b/moz/$$48.png new file mode 100644 index 0000000..bf82934 Binary files /dev/null and b/moz/$$48.png differ diff --git a/moz/$16.png b/moz/$16.png new file mode 100644 index 0000000..d2c1d4d Binary files /dev/null and b/moz/$16.png differ diff --git a/moz/$32.png b/moz/$32.png new file mode 100644 index 0000000..e192dbf Binary files /dev/null and b/moz/$32.png differ diff --git a/moz/$48.png b/moz/$48.png new file mode 100644 index 0000000..be2d54a Binary files /dev/null and b/moz/$48.png differ diff --git a/moz/128.png b/moz/128.png new file mode 100644 index 0000000..11dee12 Binary files /dev/null and b/moz/128.png differ diff --git a/moz/16.png b/moz/16.png new file mode 100644 index 0000000..e127b7c Binary files /dev/null and b/moz/16.png differ diff --git a/moz/256.png b/moz/256.png new file mode 100644 index 0000000..da1a9f2 Binary files /dev/null and b/moz/256.png differ diff --git a/moz/32.png b/moz/32.png new file mode 100644 index 0000000..e070143 Binary files /dev/null and b/moz/32.png differ diff --git a/moz/48.png b/moz/48.png new file mode 100644 index 0000000..749aa93 Binary files /dev/null and b/moz/48.png differ diff --git a/moz/96.png b/moz/96.png new file mode 100644 index 0000000..1c7b3c1 Binary files /dev/null and b/moz/96.png differ diff --git a/moz/LICENSE b/moz/LICENSE new file mode 100644 index 0000000..d79299b --- /dev/null +++ b/moz/LICENSE @@ -0,0 +1,16 @@ +Internet Systems Consortium license +=================================== + +Copyright (c) 2016, Kees Kluskens + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. diff --git a/moz/README.md b/moz/README.md new file mode 100644 index 0000000..5012573 --- /dev/null +++ b/moz/README.md @@ -0,0 +1,139 @@ +**We're currently in the process of making a v4 release which will contain the features listed below. The currently available version on the Chrome and Firefox webstore won't contain these features!** + +# Web to Plex ![Icon](src/48.png) + +![Examples](example.png) + +This browser extension searches your [Plex Media Server (PMS)](https://www.plex.tv/downloads/) for matching media on sites like [IMDb](https://imdb.com), letting you immediately open the movie or TV show in Plex, if it is available. If the item isn't found on your PMS, then a download button is added instead. + +---- + +# NZB Managers + +Optionally, you can configure NZB Manager (see support table) in the extension's options. After that, you can immediately add a TV show or movie with one click, right from your favorite site. + +## Supported Managers +| Manager | Movie Support | TV Show Support | Searchable +| ----------------------------------------------- |:-------------:|:---------------:|:----------: +| [Watcher 3](https://nosmokingbandit.github.io/) | ✔ | ❌ | ❌ +| [CouchPotato](https://couchpota.to/) | ✔ | ✔ | ❌ +| [Radarr](https://radarr.video/) | ✔ | ❌ | ✔ +| [Sonarr](https://sonarr.tv/) | ❌ | ✔ | ✔ +| [Ombi](https://ombi.io/) | ❔ | ❔ | ✔ +| [Medusa](https://pymedusa.com/) | ❌ | ✔ | ✔ +| [Sick Beard](https://sickbeard.com/) | ❌ | ✔ | ❌ + +### Key + +| ✔ | ❌ | ❔ | +| - | - | - | +| yes | no | yes (with help) | + +---- + +## Features +### Easy login +You can log into Plex using either an access token, your credentials, or Ombi (if setup). + +### Download (![download icon](https://github.com/SpaceK33z/web-to-plex/blob/master/src/16.png)) +On certain sites (denoted with the "download" icon), the user can choose to save/engage media directly, instead of waiting for their NZB manager to find the item. + +### Plex It! (![plex it icon](https://github.com/SpaceK33z/web-to-plex/blob/master/src/img/plexit.16.png)) +Click the icon to open **Plex It!** (left sidebar), click it again to add the current item(s) to your list. + +It's primary purpose is to provide a watchlist service on sites that don't support watchlists. + +### Hide Web to Plex (![hide icon](https://github.com/SpaceK33z/web-to-plex/blob/master/src/img/hide.16.png)) +Use this to hide the **Web to Plex** button. It changes the button's opacity to 10% to make it almost invisible so that it isn't as distracting on sites like Netflix. + +### Reload Web to Plex (![reload icon](https://github.com/SpaceK33z/web-to-plex/blob/master/src/img/reload.16.png)) +Use this to reload **Web to Plex** on the current page. This can sometimes fix loading issues or cache errors. + +### Plex URL +This is a *moderately advance* setting, but is very useful to know. If you know your Plex server's URL (e.g. `https://localhost:32400`), then you can specify this and avoid bandwidth usage, as the extension will communicate with Plex on your device instead of `https://app.plex.tv/`. + +### Find this | Right Click +If you aren't satisfied with a found item, or it is incorrect, you can right click the page and use the **Web to Plex | Find "XYZ"** feature to search for the item. + +The sites used as search engines (IMDb, TMDb, and TVDb) will automatically create a cached version of the information (for "Local Search" results). + +-------- + +## Supported sites + +
+
+ Sites + +1. [Movieo](http://movieo.me/) +2. [IMDb](http://imdb.com/) +3. [Trakt.tv](https://trakt.tv/) +4. [Letterboxd](https://letterboxd.com/) +5. [GoStream](https://gostream.site/)1 +6. [TV Maze](http://www.tvmaze.com/) +7. [The TVDb](https://www.thetvdb.com/) +8. [The MovieDb](https://www.themoviedb.org/) +9. [VRV](https://vrv.co/)2 +10. [Hulu](https://hulu.com/)2/3/4 +11. [Google Play Store](https://play.google.com/store/movies/) +12. Google Search (search results) +13. [iTunes](https://itunes.apple.com/)3/5 +14. [Metacritic](http://www.metacritic.com/)4 +15. [Fandango](https://www.fandango.com/) +16. [Amazon](https://www.amazon.com/)5 +17. [Vudu](https://www.vudu.com/) +18. [Verizon](https://www.tv.verizon.com/) +19. [CouchPotato](http://couchpotato.life/) +20. [Rotten Tomatoes](https://www.rottentomatoes.com/) +21. [ShowRSS](https://showrss.info/)1 (button isn't meant to appear) +22. [Netflix](https://netflix.com/)3 +23. [Toloka](https://toloka.to/)6 +24. [Shana Project](https://www.shanaproject.com/)6 +25. [My Anime List](https://myanimelist.com/)6 +26. [My Shows](https://en.myshows.me/)6 +27. [YouTube](https://youtube.com/) +28. [Flickmetrix (Cinesift)](https://flickmetrix.com/) +29. [Allociné](https://www.allocine.fr/) +30. [MovieMeter](https://www.moviemeter.nl/) +31. [JustWatch](https://justwatch.com/) +32. [Vumoo](https://vumoo.to/)1 +33. [Web to Plex](https://ephellon.github.io/web.to.plex/)2/3/4/5 +34. [Indomovie](https://indomovie.club)5/6 +35. [Kitsu](https://kitsu.io/)6 +36. [Redbox](https://redbox.com/)6 + +
+ +
+ Key + +- `1` *This site uses the `Right click | Web to Plex | Save as...` feature (file download), or a direct link (usually magnet URL).* +- `2` *This site has a loading issue, simply refresh the page if the button doesn't appear.* +- `3` *This site doesn't allow media browsing, or requires a subscription beforehand.* +- `4` *This site is network intensive (loads slowly). Be patient.* +- `5` *This site is known to update frequently, support may change without notice.* +- `6` *This site is only supported via a plugin (enabled in the settings)* + +
+ +
+ +## Installing Web to Plex + +**Download on [Chrome Webstore](https://chrome.google.com/webstore/detail/movieo-to-plex/kmcinnefmnkfnmnmijfmbiaflncfifcn).** + +**Download on [FireFox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/web-to-plex/).** + +**Download the [SRC](archive/master.zip).** + +## Requirements + ++ [**Plex Media Server v1.4.3.0**](https://www.plex.tv/downloads/#getdownload) or higher + ++ Before using the [extension](chrome://extensions), you must configure the settings + +## Issues & Contributions + +If you have any problem with the extension, please don't hesitate to [submit an issue](https://github.com/SpaceK33z/web-to-plex/issues/new/choose). + +All contributions are welcome. diff --git a/moz/_16.png b/moz/_16.png new file mode 100644 index 0000000..e563efd Binary files /dev/null and b/moz/_16.png differ diff --git a/moz/_32.png b/moz/_32.png new file mode 100644 index 0000000..d5c6c18 Binary files /dev/null and b/moz/_32.png differ diff --git a/moz/_48.png b/moz/_48.png new file mode 100644 index 0000000..d84b79f Binary files /dev/null and b/moz/_48.png differ diff --git a/moz/__test__.js b/moz/__test__.js new file mode 100644 index 0000000..68ee2d5 --- /dev/null +++ b/moz/__test__.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: '__test__' }))(); diff --git a/moz/allocine$.js b/moz/allocine$.js new file mode 100644 index 0000000..850536c --- /dev/null +++ b/moz/allocine$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'allocine' }))(); diff --git a/moz/allocine.js b/moz/allocine.js new file mode 100644 index 0000000..f0dca7b --- /dev/null +++ b/moz/allocine.js @@ -0,0 +1,29 @@ +let script = { + "url": "*://*.allocine.fr/(film|series)/*", + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('.titlebar-title').first, + year = $('.date, .meta-body font').first, + image = $('.thumbnail-img').first, + type = script.getType(); + + if(!title || !year) + return 1000; + + title = title.textContent.trim(); + image = image.src; + + year.textContent.replace(/(\d{4})/, ''); + year = +R.$1; + + return { type, title, year, image }; + }, + + "getType": () => { + let { pathname } = top.location; + + return /\/(film)\//.test(pathname)? 'film': 'show'; + }, +}; diff --git a/moz/allocine.png b/moz/allocine.png new file mode 100644 index 0000000..c5375d6 Binary files /dev/null and b/moz/allocine.png differ diff --git a/moz/amazon$.js b/moz/amazon$.js new file mode 100644 index 0000000..70796df --- /dev/null +++ b/moz/amazon$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'amazon' }))(); diff --git a/moz/amazon.js b/moz/amazon.js new file mode 100644 index 0000000..0779401 --- /dev/null +++ b/moz/amazon.js @@ -0,0 +1,55 @@ +// Web to Plex - Toloka Plugin +// Aurthor(s) - @ephellon (2019) + +/* Minimal Required Layout * + script { + url: string, + init: function => ({ type:string, title:string, year:number|null|undefined }) + } +*/ + +// REQUIRED [script:object]: The script object +let script = { + // REQUIRED [script.url]: this is what you ask Web to Plex access to; currently limited to a single domain + "url": "*://*.amazon.com/*/video/detail/*", + + // PREFERRED [script.ready]: a function to determine that the page is indeed ready + "ready": () => !$('[data-automation-id="imdb-rating-badge"], #most-recent-reviews-content > *:first-child').empty, + + // REQUIRED [script.init]: it will always be fired after the page and Web to Plex have been loaded + // OPTIONAL [ready]: if using script.ready, Web to Plex will pass a boolean of the ready state + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('[data-automation-id="title"], #aiv-content-title, .dv-node-dp-title') + .first.textContent + .replace(/(?:\(.+?\)|(\d+)|\d+\s+seasons?\s+(\d+))\s*$/gi, '') + .trim(), + // REQUIRED [title:string] + // you have access to the exposed "helper.js" file within the extension + year = +( + !(_year = $('[data-automation-id="release-year-badge"], .release-year')).empty? + _year.first.textContent.trim(): + (R.$1 || R.$2 || YEAR) + ), + // PREFERRED [year:number, null, undefined] + image = ( + (_image = $('.av-bgimg__div, div[style*="sgp-catalog-images"]')).empty? + $('.av-fallback-packshot img').src: + getComputedStyle(_image.first).backgroundImage.replace(/[^]*url\((["']?)(.+?)\1\)[^]*/i, '$2') + ), + // the rest of the code is up to you, but should be limited to a layout similar to this + type = script.getType(); + + // REQUIRED [{ type:'movie', 'show'; title:string; year:number }] + // PREFERRED [{ image:string; IMDbID:string; TMDbID:string, number; TVDbID:string, number }] + return { type, title, year, image }; + }, + + // OPTIONAL: the rest of this code is purely for functionality + "getType": () => { + return !$('[data-automation-id*="season"], [class*="season"], [class*="episode"], [class*="series"]').empty? + 'tv': + 'movie' + }, +}; diff --git a/moz/amazon.png b/moz/amazon.png new file mode 100644 index 0000000..7f4afc7 Binary files /dev/null and b/moz/amazon.png differ diff --git a/moz/background.js b/moz/background.js new file mode 100644 index 0000000..72d8df6 --- /dev/null +++ b/moz/background.js @@ -0,0 +1,1031 @@ +/* global chrome */ +let BACKGROUND_DEVELOPER = false; + +let external = {}, + __context_parent__, + __context_save_element__, + BACKGROUND_TERMINAL = + BACKGROUND_DEVELOPER? + console: + { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m }; + +let date = (new Date), + YEAR = date.getFullYear(), + MONTH = date.getMonth() + 1, + DATE = date.getDate(); + +let BACKGROUND_STORAGE = browser.storage.sync || browser.storage.local; +let BACKGROUND_CONFIGURATION; + + +// returns the proper CORS mode of the URL +let cors = url => ((/^(https|sftp)\b/i.test(url) || /\:(443|22)\b/? '': 'no-') + 'cors'); + +// Create a Crypto-Key +// new Key(number:integer, string:symbol) -> string +class Key { + constructor(length = 8, symbol = '') { + let values = []; + + window.crypto.getRandomValues(new Uint32Array(16)).forEach((value, index, array) => values.push(value.toString(36))); + + return this.length = length, this.value = values.join(symbol); + } + + rehash(length, symbol) { + if(length <= 0) + length = this.length; + + return this.value = new Key(length, symbol); + } +} + +// Session instances +let SessionKey = new Key(16), // create a session key + SessionState = false; // has this been run already? + +// Generate request headers (for fetches) +// new Headers({username, password}) -> object +class Headers { + constructor(Authorization) { + let headers = { Accept: 'application/json' }; + + if (!Authorization) + return headers; + + return { + Authorization: `Basic ${ btoa(`${ Authorization.username }:${ Authorization.password }`) }`, + ...headers + }; + } +} + +// Change the badge status +// ChangeStatus({ MovieOrShowID, MovieOrShowTitle, MovieOrShowType, MovieOrShowIDProvider, MovieOrShowYear, LinkURL, FileType, FilePath }) -> undefined +function ChangeStatus({ ITEM_ID, ITEM_TITLE, ITEM_TYPE, ID_PROVIDER, ITEM_YEAR, ITEM_URL = '', FILE_TYPE = '', FILE_PATH }) { + + let FILE_TITLE = ITEM_TITLE.replace(/\-/g, ' ').replace(/[\s\:]{2,}/g, ' - ').replace(/[^\w\s\-\']+/g, ''), + // File friendly title + SEARCH_TITLE = ITEM_TITLE.replace(/[\-\s]+/g, '-').replace(/\s*&\s*/g, ' and ').replace(/[^\w\-\'\*\#]+/g, ''), + // Search friendly title + SEARCH_PROVIDER = /\b(tv|show|series)\b/i.test(ITEM_TYPE)? 'GG': /^im/i.test(ID_PROVIDER)? 'VO': /^tm/i.test(ID_PROVIDER)? 'GX': 'GG'; + + ITEM_ID = (ITEM_ID && !/^tt$/i.test(ITEM_ID)? ITEM_ID: '') + ''; + ITEM_ID = ITEM_ID.replace(/^.*\b(tt\d+)\b.*$/, '$1').replace(/^.*\bid=(\d+)\b.*$/, '$1').replace(/^.*(?:movie|tv|(?:tv-?)?(?:shows?|series|episodes?))\/(\d+).*$/, '$1'); + + external = { ...external, ID_PROVIDER, ITEM_ID, ITEM_TITLE, ITEM_YEAR, ITEM_URL, ITEM_TYPE, SEARCH_PROVIDER, SEARCH_TITLE, FILE_PATH, FILE_TITLE, FILE_TYPE }; + + browser.browserAction.setBadgeText({ + text: ID_PROVIDER + }); + + browser.browserAction.setBadgeBackgroundColor({ + color: (ITEM_ID? '#f45a26': '#666666') + }); + + browser.contextMenus.update('W2P', { + title: `Find "${ ITEM_TITLE } (${ ITEM_YEAR || YEAR })"` + }); + + for(let databases = 'IM TM TV'.split(' '), length = databases.length, index = 0, database; index < length; index++) + browser.contextMenus.update('W2P-' + (database = databases[index]), { + title: ( + ((ID_PROVIDER == (database += 'Db')) && ITEM_ID)? + `Open in ${ database } (${ (+ITEM_ID? '#': '') + ITEM_ID })`: + `Find in ${ database }` + ), + checked: false + }); + + browser.contextMenus.update('W2P-XX', { + title: `Find on ${ (SEARCH_PROVIDER == 'VO'? 'Vumoo': SEARCH_PROVIDER == 'GX'? 'GoStream': 'Google') }`, + checked: false + }); +} + + +// get the saved options +function getConfiguration() { + return new Promise((resolve, reject) => { + function handleConfiguration(options) { + if((!options.plexToken || !options.servers) && !options.IGNORE_PLEX) + return reject(new Error('Required options are missing')), + null; + + let server, o; + + if(!options.IGNORE_PLEX) { + // For now we support only one Plex server, but the options already + // allow multiple for easy migration in the future. + server = options.servers[0]; + o = { + server: { + ...server, + // Compatibility for users who have not updated their settings yet. + connections: server.connections || [{ uri: server.url }] + }, + ...options + }; + } else { + o = options; + } + + resolve(o); + } + + BACKGROUND_STORAGE.get(null, options => { + if(browser.runtime.lastError) + browser.runtime.lastError.message || + browser.storage.local.get(null, handleOptions); + else + handleConfiguration(options); + }); + }); +} + +// self explanatory, returns an object; sets the configuration variable +function parseConfiguration() { + return getConfiguration().then(options => { + BACKGROUND_CONFIGURATION = options; + + if((BACKGROUND_DEVELOPER = options.DeveloperMode) && !parseConfiguration.gotConfig) { + parseConfiguration.gotConfig = true; + BACKGROUND_TERMINAL = + BACKGROUND_DEVELOPER? + console: + { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m }; + + BACKGROUND_TERMINAL.warn(`BACKGROUND_DEVELOPER: ${BACKGROUND_DEVELOPER}`); + } + + return options; + }, error => { throw error }); +} + +(async() => { + await parseConfiguration(); +})(); + +/** CouchPotato - Movies **/ +// At this point you might want to think, WHY would you want to do +// these requests in a background page instead of the content script? +// This is because Movieo is served over HTTPS, so it won't accept requests to +// HTTP servers. Unfortunately, many people use CouchPotato over HTTP. +function Open_CouchPotato(request, sendResponse) { + fetch(`${ request.url }?id=${ request.imdbId }`, { + headers: new Headers(request.basicAuth), + mode: cors(request.url) + }) + .then(response => response.json()) + .then(json => { + sendResponse({ success, status: (success? json.media.status: null) }); + }) + .catch(error => { + sendResponse({ error: String(error), location: '@0B: Open_CouchPotato' }); + }); +} + +function Push_CouchPotato(request, sendResponse) { + fetch(`${ request.url }?identifier=${ request.imdbId }`, { + headers: new Headers(request.basicAuth), + mode: cors(request.url) + }) + .then(response => response.json()) + .catch(error => sendResponse({ error: 'Item not found', location: '@0B: Push_CouchPotato => fetch.then.catch', silent: true })) + .then(response => { + sendResponse({ success: response.success }); + }) + .catch(error => { + sendResponse({ error: String(error) , location: '@0B: Push_CouchPotato'}); + }); +} + +/** Watcher - Movies **/ +function Push_Watcher(request, sendResponse) { + let headers = { + 'Content-Type': 'application/json', + 'X-Api-Key': request.token, + ...(new Headers(request.basicAuth)) + }, + id = (/^(tt)?$/.test(request.imdbId)? request.tmdbId: request.imdbId), + // if the IMDbID is empty, jump to the TMDbID + query = (/^tt\d+$/i.test(id)? 'imdbid': /^\d+$/.test(id)? 'tmdbid': (id = encodeURI(`${request.title} ${request.year}`), 'term')), + // if the IMDbID is empty, use "&tmdbid={ id }" + // if the IMDbID isn't empty, use "&imdbid={ id }" + // otherwise, use "&term={ title } { year }" + debug = { headers, query, request }; + // setup a stack trace for debugging + + fetch(debug.url = `${ request.url }?apikey=${ request.token }&mode=addmovie&${ query }=${ id }`) + .then(response => response.json()) + .catch(error => sendResponse({ error: 'Movie not found', location: '@0B: Push_Watcher => fetch.then.catch', silent: true })) + .then(response => { + if((response.response + "") == "true") + return sendResponse({ + success: `Added to Watcher (${ request.StoragePath })` + }); + + throw new Error(response.error); + }) + .catch(error => { + sendResponse({ + error: String(error), + location: `@0B: Push_Watcher => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, + debug + }); + }); +} + +/** Radarr - Movies **/ +function Push_Radarr(request, sendResponse) { + let headers = { + 'Content-Type': 'application/json', + 'X-Api-Key': request.token, + ...(new Headers(request.basicAuth)) + }, + id = (/^(tt)?$/.test(request.imdbId)? request.tmdbId: request.imdbId), + // if the IMDbID is empty, jump to the TMDbID + query = (/^tt\d+$/i.test(id)? 'imdb?imdbid': /^\d+$/.test(id)? 'tmdb?tmdbid': (id = encodeURI(`${request.title} ${request.year}`), 'term')), + // if the IMDbID is empty, use "/tmdb?tmdbid={ id }" + // if the IMDbID isn't empty, use "/imdb?imdbid={ id }" + // otherwise, use "&term={ title } { year }" + debug = { headers, query, request }; + // setup a stack trace for debugging + + fetch(debug.url = `${ request.url }lookup/${ query }=${ id }&apikey=${ request.token }`) + .then(response => response.json()) + .catch(error => sendResponse({ error: 'Movie not found', location: '@0B: Push_Radarr => fetch.then.catch', silent: true })) + .then(data => { + let body, + // Monitor, search, and download movie ASAP + props = { + monitored: true, + minimumAvailability: 'preDB', + qualityProfileId: request.QualityID, + rootFolderPath: request.StoragePath, + addOptions: { + searchForMovie: true + } + }; + + if (!data instanceof Array && !data.length && !data.title) { + throw new Error('Movie not found'); + } else if(data.length) { + body = { + ...data[0], + ...props + }; + } else if(data.title) { + body = { + ...data, + ...props + }; + } + + BACKGROUND_TERMINAL.group('Generated URL'); + BACKGROUND_TERMINAL.log('URL', request.url); + BACKGROUND_TERMINAL.log('Head', headers); + BACKGROUND_TERMINAL.log('Body', body); + BACKGROUND_TERMINAL.groupEnd(); + + return debug.body = body; + }) + .then(body => { + return fetch(`${ request.url }?apikey=${ request.token }`, debug.requestHeaders = { + method: 'POST', + mode: cors(request.url), + body: JSON.stringify(body), + headers + }); + }) + .then(response => response.text()) + .then(data => { + debug.data = + data = JSON.parse(data || `{"path":"${ request.StoragePath.replace(/\\/g, '\\\\') }${ request.title } (${ request.year })"}`); + + if (data && data[0] && data[0].errorMessage) { + sendResponse({ + error: data[0].errorMessage, + location: `@0B: Push_Radarr => fetch("${ request.url }", { headers }).then(data => { if })`, + debug + }); + } else if (data && data.path) { + sendResponse({ + success: 'Added to ' + data.path + }); + } else { + sendResponse({ + error: 'Unknown error', + location: `@0B: Push_Radarr => fetch("${ request.url }", { headers }).then(data => { else })`, + debug + }); + } + }) + .catch(error => { + sendResponse({ + error: String(error), + location: `@0B: Push_Radarr => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, + debug + }); + }); +} + +/** Sonarr - TV Shows **/ +function Push_Sonarr(request, sendResponse) { + let headers = { + 'Content-Type': 'application/json', + 'X-Api-Key': request.token, + ...(new Headers(request.basicAuth)) + }, + id = request.tvdbId, + query = encodeURIComponent(`tvdb:${ id }`), + debug = { headers, query, request }; + // setup stack trace for debugging + + fetch(debug.url = `${ request.url }lookup?apikey=${ request.token }&term=${ query }`) + .then(response => response.json()) + .catch(error => sendResponse({ error: 'TV Show not found', location: '@0B: Push_Sonarr => fetch.then.catch', silent: true })) + .then(data => { + if (!data instanceof Array || !data.length) + throw new Error('TV Show not found'); + + // Monitor, search, and download series ASAP + let body = { + ...data[0], + monitored: true, + seasonFolder: true, + minimumAvailability: 'preDB', + qualityProfileId: request.QualityID, + rootFolderPath: request.StoragePath, + addOptions: { + searchForMissingEpisodes: true + } + }; + + BACKGROUND_TERMINAL.group('Generated URL'); + BACKGROUND_TERMINAL.log('URL', request.url); + BACKGROUND_TERMINAL.log('Head', headers); + BACKGROUND_TERMINAL.log('Body', body); + BACKGROUND_TERMINAL.groupEnd(); + + return debug.body = body; + }) + .then(body => { + return fetch(`${ request.url }?apikey=${ request.token }`, debug.requestHeaders = { + method: 'POST', + mode: cors(request.url), + body: JSON.stringify(body), + headers + }); + }) + .then(response => response.text()) + .then(data => { + debug.data = + data = JSON.parse(data || `{"path":"${ request.StoragePath.replace(/\\/g, '\\\\') }${ request.title } (${ request.year })"}`); + + if (data && data[0] && data[0].errorMessage) { + sendResponse({ + error: data[0].errorMessage, + location: `@0B: Push_Sonarr => fetch("${ request.url }", { headers }).then(data => { if })`, + debug + }); + } else if (data && data.path) { + sendResponse({ + success: 'Added to ' + data.path + }); + } else { + sendResponse({ + error: 'Unknown error', + location: `@0B: Push_Sonarr => fetch("${ request.url }", { headers }).then(data => { else })`, + debug + }); + } + }) + .catch(error => { + sendResponse({ + error: String(error), + location: `@0B: Push_Sonarr => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, + debug + }); + }); +} + +/** Medusa - TV Shows **/ +function Push_Medusa(request, sendResponse) { + let headers = { + 'Content-Type': 'application/json', + 'X-Api-Key': request.token, + ...(new Headers(request.basicAuth)) + }, + id = request.tvdbId, + query = request.title.replace(/\s+/g, '+'), + debug = { headers, query, request }; + // setup stack trace for debugging + + fetch(debug.url = `${ request.root }internal/searchIndexersForShowName?api_key=${ request.token }&indexerId=0&query=${ query }`) + .then(response => response.json()) + .catch(error => sendResponse({ error: 'TV Show not found', location: '@0B: Push_Medusa => fetch.then.catch', silent: true })) + .then(data => { + data = data.results; + + if (!data instanceof Array || !data.length) + throw new Error('TV Show not found'); + + // Monitor, search, and download series ASAP + let body = data[0].join('|'); + + BACKGROUND_TERMINAL.group('Generated URL'); + BACKGROUND_TERMINAL.log('URL', request.url); + BACKGROUND_TERMINAL.log('Head', headers); + BACKGROUND_TERMINAL.log('Body', body); + BACKGROUND_TERMINAL.groupEnd(); + + return debug.body = body; + }) + .then(body => { + return fetch(`${ request.url }`, debug.requestHeaders = { + method: 'POST', + mode: cors(request.url), + body: JSON.stringify({ id: { tvdb: request.tvdbId } }), + headers + }); + }) + .then(response => response.text()) + .then(data => { + let path = request.StoragePath.replace(/\\?$/, '\\'); + + debug.data = + data = JSON.parse(data || `{"path":"${ path }${ request.title } (${ request.year })"}`); + + if (data && data.error) { + sendResponse({ + error: data.error, + location: `@0B: Push_Medusa => fetch("${ request.url }", { headers }).then(data => { if })`, + debug + }); + } else if (data && data.id) { + sendResponse({ + success: `Added to ${ path }${ request.title }(${ request.year })` + }); + } else { + sendResponse({ + error: 'Unknown error', + location: `@0B: Push_Medusa => fetch("${ request.url }", { headers }).then(data => { else })`, + debug + }); + } + }) + .catch(error => { + sendResponse({ + error: String(error), + location: `@0B: Push_Medusa => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, + debug + }); + }); +} + +/** Medusa - TV Shows **/ +function addMedusa(request, sendResponse) { + let headers = { + 'Content-Type': 'application/json', + 'X-Api-Key': request.token, + ...(new Headers(request.basicAuth)) + }, + id = request.tvdbId, + query = request.title.replace(/\s+/g, '+'), + debug = { headers, query, request }; + // setup stack trace for debugging + + fetch(debug.url = `${ request.root }internal/searchIndexersForShowName?api_key=${ request.token }&indexerId=0&query=${ query }`) + .then(response => response.json()) + .catch(error => sendResponse({ error: 'TV Show not found', location: 'addMedusa => fetch.then.catch', silent: true })) + .then(data => { + data = data.results; + + if (!data instanceof Array || !data.length) + throw new Error('TV Show not found'); + + // Monitor, search, and download series ASAP + let body = data[0].join('|'); + + BACKGROUND_TERMINAL.group('Generated URL'); + BACKGROUND_TERMINAL.log('URL', request.url); + BACKGROUND_TERMINAL.log('Head', headers); + BACKGROUND_TERMINAL.log('Body', body); + BACKGROUND_TERMINAL.groupEnd(); + + return debug.body = body; + }) + .then(body => { + return fetch(`${ request.url }`, debug.requestHeaders = { + method: 'POST', + mode: cors(request.url), + body: JSON.stringify({ id: { tvdb: request.tvdbId } }), + headers + }); + }) + .then(response => response.text()) + .then(data => { + let path = request.StoragePath.replace(/\\?$/, '\\'); + + debug.data = + data = JSON.parse(data || `{"path":"${ path }${ request.title } (${ request.year })"}`); + + if (data && data.error) { + sendResponse({ + error: data.error, + location: `addMedusa => fetch("${ request.url }", { headers }).then(data => { if })`, + debug + }); + } else if (data && data.id) { + sendResponse({ + success: `Added to ${ path }${ request.title }(${ request.year })` + }); + } else { + sendResponse({ + error: 'Unknown error', + location: `addMedusa => fetch("${ request.url }", { headers }).then(data => { else })`, + debug + }); + } + }) + .catch(error => { + sendResponse({ + error: String(error), + location: `addMedusa => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, + debug + }); + }); +} + +/** Sick Beard - TV Shows **/ +function Push_SickBeard(request, sendResponse) { + let headers = { + 'Content-Type': 'application/json', + 'X-Api-Key': request.token, + ...(new Headers(request.basicAuth)) + }, + id = request.tvdbId, + query = `tvdbid=${ id }`, + path = (`${ request.StoragePath }\\${ request.title }`).replace(/\\\\/g, '\\'), + debug = { headers, query, request }; + // setup stack trace for debugging + + fetch(debug.url = `${ request.url }?cmd=sb.searchtvdb&${ query }`) + .then(response => response.json()) + .catch(error => sendResponse({ error: 'TV Show not found', location: '@0B: Push_SickBeard => fetch.then.catch', silent: true })) + .then(data => { + if (!/^success$/i.test(data.result)) + throw new Error('TV Show not found'); + + data = data.data.results; + + // Monitor, search, and download series ASAP + let body = formify({ + tvdbid: id, + initial: request.QualityID, + location: encodeURIComponent(path), + status: 'wanted', + }); + + BACKGROUND_TERMINAL.group('Generated URL'); + BACKGROUND_TERMINAL.log('URL', request.url); + BACKGROUND_TERMINAL.log('Head', headers); + BACKGROUND_TERMINAL.log('Body', body); + BACKGROUND_TERMINAL.groupEnd(); + + return debug.body = body; + }) + .then(async body => { + await fetch(`${ request.url }?cmd=sb.addrootdir&${ body }`); + + return fetch(`${ request.url }?cmd=show.${ request.exists? 'addexisting': 'addnew' }&${ body }`, debug.requestHeaders = { + method: 'POST', + mode: cors(request.url), + // body: JSON.stringify(body), + headers + }); + }) + .then(response => response.text()) + .then(results => { + debug.data = + results = JSON.parse(results || `{"data":{},message:"",result:""}`); + + let { data, message, result } = results; + + data.path = `${ request.StoragePath }${ request.title } (${ request.year })`; + + if (data && !/^success$/i.test(result) && message) { + sendResponse({ + error: message, + location: `@0B: Push_SickBeard => fetch("${ request.url }", { headers }).then(results => { if })`, + debug + }); + } else if (data && data.path) { + sendResponse({ + success: 'Added to ' + data.path + }); + } else { + sendResponse({ + error: 'Unknown error', + location: `@0B: Push_SickBeard => fetch("${ request.url }", { headers }).then(results => { else })`, + debug + }); + } + }) + .catch(error => { + sendResponse({ + error: String(error), + location: `@0B: Push_SickBeard => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, + debug + }); + }); +} + +/** Ombi* - TV Shows/Movies **/ +function Push_Ombi(request, sendResponse) { + let headers = { + 'Content-Type': 'application/json', + 'ApiKey': request.token, + ...(new Headers) + }, + type = request.contentType, + id = (type == 'movie'? request.tmdbId: request.tvdbId), + body = ({ [type == 'movie'? 'theMovieDbId': 'theTvDbId']: id }), + debug = { headers, request }; + // setup stack trace for debugging + + if(request.contentType == 'movie' && (id || null) === null) + sendResponse({ error: 'Invalid TMDbID', location: '@0B: Push_Ombi => if', silent: true }); + else if((id || null) === null) + sendResponse({ error: 'Invalid TVDbID', location: '@0B: Push_Ombi => else if', silent: true }); + + fetch(debug.url = request.url, { + method: 'POST', + mode: cors(request.url), + body: JSON.stringify(body), + headers + }) + .catch(error => sendResponse({ error: `${ type } not found`, location: '@0B: Push_Ombi => fetch.then.catch', silent: true })) + .then(response => response.text()) + .then(data => { + debug.data = + data = JSON.parse(data); + + if (data && data.isError) { + if(/already +been +requested/i.test(data.errorMessage)) + sendResponse({ + success: 'Already requested on Ombi' + }); + else + sendResponse({ + error: data.errorMessage, + location: `@0B: Push_Ombi => fetch("${ request.url }", { headers }).then(data => { if })`, + debug + }); + } else if (data && data.path) { + sendResponse({ + success: 'Added to Ombi' + }); + } else { + sendResponse({ + error: 'Unknown error', + location: `@0B: Push_Ombi => fetch("${ request.url }", { headers }).then(data => { else })`, + debug + }); + } + }) + .catch(error => { + sendResponse({ + error: String(error), + location: `@0B: Push_Ombi => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, + debug + }); + }); +} + +// Unfortunately the native Promise.race does not work as you would suspect. +// If one promise (Plex request) fails, we still want the other requests to continue racing. +// See https://www.jcore.com/2016/12/18/promise-me-you-wont-use-promise-race/ for an explanation +function PromiseRace(promises) { + if (!~promises.length) { + return Promise.reject('Cannot start a race without promises!'); + } + + // There is no way to know which promise is rejected. + // So we map it to a new promise to return the index when it fails + let Promises = promises.map((promise, index) => + promise.catch(() => { + throw index; + }) + ); + + return Promise.race(Promises) + .catch(index => { + // The promise has rejected, remove it from the list of promises and just continue the race. + let promise = promises.splice(index, 1)[0]; + + promise.catch(error => BACKGROUND_TERMINAL.log(`Plex request #${ index } failed:`, error)); + return PromiseRace(promises); + }); +} + +function $Search_Plex(connection, headers, options) { + let type = options.type || 'movie', + url = `${ connection.uri }/hubs/search`, + field = options.field || 'title'; + + if(!options.title) + return {}; + + if(/movie|film|cinema|theat[re]{2}/i.test(type)) + type = 'movie'; + else if(/tv|show|series|episode/i.test(type)) + type = 'show'; + + // Letterboxd can contain special white-space characters. Plex doesn't like this. + let title = encodeURIComponent(options.title.replace(/\s+/g, ' ')), + finalURL = `${ url }?query=${ field }:${ title }`; + + // BACKGROUND_TERMINAL.warn(`Fetching <${ JSON.stringify(headers) } ${ finalURL } >`); + return fetch(finalURL, { headers }) + .then(response => response.json()) + .then(data => { + let Hub = data.MediaContainer.Hub.find(hub => hub.type === type); + + if (!Hub || !Hub.Metadata) { + return { found: false }; + } + + // We only want to search in Plex libraries with the type "Movie", i.e. not the type "Other Videos". + // Weirdly enough Plex doesn't seem to have an easy way to filter those libraries so we invent our own hack. + let movies = Hub.Metadata.filter( + meta => + meta.Directory || + meta.Genre || + meta.Country || + meta.Role || + meta.Writer + ), + strip = (string) => string.replace(/\W+/g, '').toLowerCase(); + + // This is messed up, but Plex's definition of a year is year when it was available, + // not when it was released (which is Movieo's definition). + // For examples, see Bone Tomahawk, The Big Short, The Hateful Eight. + // So we'll first try to find the movie with the given year, and then + 1 it. + // Added [strip] to prevent mix-ups, see: "Kingsman: The Golden Circle" v. "The Circle" + let media = movies.find(meta => ((meta.year == +options.year) && strip(meta.title) == strip(options.title))), + key = null; + + if (!media) { + media = movies.find(meta => ((meta.year == +options.year + 1) && strip(meta.title) == strip(options.title))); + } + + key = !!media? media.key.replace('/children', ''): key; + + return { + found: !!media, + key + }; + }) + .catch(error => { throw error }); +} + +async function Search_Plex(request, sendResponse) { + let { options, serverConfig } = request, + headers = { + 'X-Plex-Token': serverConfig.token, + 'Accept': 'application/json' + }; + + // Try all Plex connection URLs + let requests = serverConfig.connections.map(connection => + $Search_Plex(connection, headers, options) + ); + + try { + // See what connection URL finishes the request first and pick that one. + // TODO: optimally, as soon as the first request is finished, all other requests would be cancelled using AbortController. + let result = await PromiseRace(requests); + + sendResponse(result); + } catch (error) { + sendResponse({ error: String(error), location: '@0B: Search_Plex' }); + } +} + +// Chrome is f**king retarted... +// Instead of having an object returned (for the context-menu) +// You have to make API calls on ALL clicks... +browser.contextMenus.onClicked.addListener(item => { + if(!/^W2P/i.test(item.menuItemId)) return; + + let url = "", dnl = false, + db = item.menuItemId.slice(-2).toLowerCase(), + pv = external.ID_PROVIDER.slice(0, 2).toLowerCase(), + qu = external.ITEM_ID, + tl = external.SEARCH_TITLE, + yr = external.ITEM_YEAR, + tt = external.ITEM_TITLE, + lt = external.FILE_TITLE, + ft = external.FILE_TYPE, + fp = external.FILE_PATH, + p = (s, r = '+') => s.replace(/-/g, r); + + switch(db) { + case 'im': + url = (qu && pv == 'im')? + `imdb.com/title/${ qu }/`: + `imdb.com/find?ref_=nv_sr_fn&s=all&q=${ tt }`; + break; + case 'tm': + url = (qu && pv == 'tm')? + `themoviedb.org/${ external.ITEM_TYPE == 'show'? 'tv': 'movie' }/${ qu }`: + `themoviedb.org/search?query=${ tt }`; + break; + case 'tv': + url = (qu && pv == 'tv')? + `thetvdb.com/series/${ tl }#${ qu }`: // TVDb accepts either: a title, or a series number... but only one + `thetvdb.com/search?q=${ p(tl) }`; + break; + case 'xx': + url = external.SEARCH_PROVIDER == 'VO'? + `google.com/search?q=${ p(tl) }+site:vumoo.to`: + external.SEARCH_PROVIDER == 'GX'? + `gostream.site/?s=${ p(tl) }`: + `google.com/search?q="${ p(tl, ' ') } ${ yr }"+${ pv }db`; + break; + case 'dl': + dnl = true; + url = external.ITEM_URL; + break; + default: return; break; + } + + if(!dnl) + window.open(`https://${ url }`, '_blank'); + else if (dnl) + // try/catch won't work here, so use the first download's callback as an error catcher + browser.downloads.download({ + url: item.href, + filename: `${ fp }${ lt } (${ yr }).${ ft }`, + saveAs: true + }, id => { + if(id == undefined || id == null) + browser.downloads.download({ + url: item.href, + saveAs: true + }); + }); +}); + +browser.runtime.onMessage.addListener((request, sender, callback) => { + BACKGROUND_TERMINAL.log('From:', sender); + + let item = (request? request.options || request: {}), + ITEM_TITLE = item.title, + ITEM_YEAR = item.year, + ITEM_TYPE = item.type, + ID_PROVIDER = (i=>{for(let p in i)if(/^TV(Db)?/i.test(p)&&i[p])return'TVDb';else if(/^TM(Db)?/i.test(p)&&i[p])return'TMDb';return'IMDb'})(item), + ITEM_URL = (item.href || ''), + FILE_TYPE = (item.tail || 'mp4'), + FILE_PATH = (item.path || ''), + ITEM_ID = ((i, I)=>{for(let p in i)if(RegExp('^'+I,'i').test(p))return i[p]})(item, ID_PROVIDER); + + try { + switch (request.type) { + case 'SEARCH_PLEX': + Search_Plex(request, callback); + return true; + + case 'VIEW_COUCHPOTATO': + Open_CouchPotato(request, callback); + return true; + + case 'PUSH_COUCHPOTATO': + Push_CouchPotato(request, callback); + return true; + + case 'PUSH_RADARR': + Push_Radarr(request, callback); + return true; + + case 'PUSH_SONARR': + Push_Sonarr(request, callback); + return true; + + case 'PUSH_MEDUSA': + Push_Medusa(request, callback); + return true; + + case 'PUSH_WATCHER': + Push_Watcher(request, callback); + return true; + + case 'PUSH_OMBI': + Push_Ombi(request, callback); + return true; + + case 'PUSH_SICKBEARD': + Push_SickBeard(request, callback); + return true; + + case 'OPEN_OPTIONS': + browser.runtime.openOptionsPage(); + return true; + + case 'SEARCH_FOR': + if(ITEM_TITLE && ITEM_TYPE) + ChangeStatus({ ITEM_ID, ITEM_TITLE, ITEM_TYPE, ID_PROVIDER, ITEM_YEAR, ITEM_URL, FILE_TYPE, FILE_PATH }); + return true; + + case 'SAVE_AS': + browser.contextMenus.update('W2P-DL', { + title: `Save as "${ ITEM_TITLE } (${ ITEM_YEAR })" (${ FILE_TYPE })` + }); + return true; + + case 'DOWNLOAD_FILE': + let FILE_TITLE = ITEM_TITLE.replace(/\-/g, ' ').replace(/[\s\:]{2,}/g, ' - ').replace(/[^\w\s\-\']+/g, ''); + + // no try/catch, use callback for that + browser.downloads.download({ + url: item.href, + filename: `${ FILE_TITLE } (${ ITEM_YEAR }).${ FILE_TYPE }`, + saveAs: true + }, id => { + // Error Occured + if(id == undefined || id == null) + browser.downloads.download({ + url: item.href, + filename: `${ FILE_TITLE } (${ ITEM_YEAR })`, + saveAs: true + }); + }); + return true; + + case 'PLUGIN': + case 'SCRIPT': + case '_INIT_': + case '$INIT$': + case 'FOUND': + case 'GRANT_PERMISSION': + /* These are meant to be handled by plugn.js */ + return false; + + default: + BACKGROUND_TERMINAL.warn(`Unknown event [${ request.type }]`); + return false; + } + } catch (error) { + return callback(String(error)); + } +}); + +// If background.js is already running, ignore the new state +// otherwise, use the following to start up +if(SessionState === false) { + SessionState = true; + + __context_parent__ = browser.contextMenus.create({ + id: 'W2P', + title: 'Web to Plex' + }); + + __context_save_element__ = browser.contextMenus.create({ + id: 'W2P-DL', + title: 'Nothing to Save' + }); + + // Standard search engines + for(let array = 'IM TM TV'.split(' '), DL = {}, length = array.length, index = 0, item; index < length; index++) + browser.contextMenus.create({ + id: 'W2P-' + (item = array[index]), + parentId: __context_parent__, + title: `Using ${ item }Db`, + type: 'checkbox', + checked: true // implement a way to use the checkboxes? + }); + + // Non-standard search engines + browser.contextMenus.create({ + id: 'W2P-XX', + parentId: __context_parent__, + title: `Using best guess`, + type: 'checkbox', + checked: true // implement a way to use the checkboxes? + }); + +} + +// turn object into URL paramaeters: +// { data: data, ... } => data=data&... +function formify(data) { + let body = []; + for(let key in data) + body.push(`${ key }=${ data[key] }`); + return body.join('&'); +} + +if(browser.runtime.lastError) + BACKGROUND_TERMINAL.warn(`Last known error: ${ browser.runtime.lastError.message }`); diff --git a/moz/close.16.png b/moz/close.16.png new file mode 100644 index 0000000..70283c6 Binary files /dev/null and b/moz/close.16.png differ diff --git a/moz/close.48.png b/moz/close.48.png new file mode 100644 index 0000000..857589e Binary files /dev/null and b/moz/close.48.png differ diff --git a/moz/common.css b/moz/common.css new file mode 100644 index 0000000..3479a6c --- /dev/null +++ b/moz/common.css @@ -0,0 +1,622 @@ +/** Common CSS + * Web to Plex + */ + /* Basic/Global Styling */ + [class*="web-to-plex"]::-webkit-scrollbar, [class*="web-to-plex"]::-moz-scrollbar { + width: 10px !important; + } + + [class*="web-to-plex"]::-webkit-scrollbar-thumb, [class*="web-to-plex"]::-moz-scrollbar-thumb { + min-height: 50px !important; + background: rgba(255, 255, 255, 0.15) !important; + border: 2px solid rgba(0, 0, 0, 0) !important; + border-radius: 8px !important; + background-clip: padding-box !important; + } + + [class*="web-to-plex"]::-webkit-scrollbar-track, [class*="web-to-plex"]::-moz-scrollbar-track { + background: #0000 !important; + } + + [class*="web-to-plex"]::-webkit-input-placeholder, [class*="web-to-plex"]::-moz-placeholder, [class*="web-to-plex"]:-moz-placeholder { + color: #999 !important; + } + + [class*="web-to-plex"] input[type="text"], [class*="web-to-plex"] input[type="password"], [class*="web-to-plex"] select { + width: 30vw !important; + line-height: 1.5em !important; + transition: background 0.2s !important; + display: block !important; + height: 38px !important; + padding: 6px 12px !important; + font-size: 16px !important; + color: #eee !important; + vertical-align: middle; + background: rgba(255, 255, 255, 0.25) !important; + border: 3px solid rgba(0, 0, 0, 0) !important; + border-radius: 3px !important; + font-family: inherit !important; + margin: 0 !important; + } + + [class*="web-to-plex"] select { + margin-left: 10px !important; + font-size: 16px !important; + line-height: inherit !important; + text-transform: none !important; + } + + [class*="web-to-plex"] option { + background: #3f4245 !important; + } + + [class*="web-to-plex"] button { + padding: 10px 18px !important; + font-size: 16px !important; + line-height: 1.33 !important; + border-radius: 3px !important; + font-family: inherit !important; + text-transform: uppercase !important; + border: 0 !important; + box-shadow: none !important; + position: relative !important; + overflow: hidden !important; + color: #fff; + background: #cc7b19; + margin-bottom: 0 !important; + font-weight: 400 !important; + vertical-align: middle; + cursor: pointer !important; + white-space: nowrap; + user-select: none; + transition: all 0.1s !important; + } + + [class*="web-to-plex"] button:hover { + background: #e59029; + } + + [class*="web-to-plex"] input::placeholder, [class*="web-to-plex"] input:placeholder { + color: #999 !important; + } + + [class*="web-to-plex"][disabled], [class*="web-to-plex"] [disabled] { + cursor: not-allowed !important; + color: #909090EE !important; + } + +/* Web to Plex notifications */ +.web-to-plex-notification { + background: #F45A26 !important; + border-radius: 4px !important; + color: #FFF !important; + cursor: pointer !important; + display: block !important; + font-family: arial, verdana, sans-serif !important; + font-size: 20px !important; + text-align: center !important; + + position: fixed !important; + left: 50% !important; + margin-left: -175px !important; + padding: 10px !important; + top: 80px !important; + + width: 350px !important; + z-index: 999999999 !important; +} + +/* Web to Plex general information notifications */ +.web-to-plex-notification.info { + background: #666 !important; +} + +/* Web to Plex update notifications */ +.web-to-plex-notification.update { + background: #2A2AFF !important; +} + +/* Web to Plex success notifications */ +.web-to-plex-notification.success { + background: #03BDF9 !important; +} + +/* Web to Plex error/warning notifications */ +.web-to-plex-notification.warning, .web-to-plex-notification.error { + background: #FF2A2A !important; +} + +/* Web to Plex prompts */ +.web-to-plex-prompt { + background: #0008 !important; + box-sizing: border-box !important; + color: #eee !important; + display: block !important; + font-family: Open Sans Regular, Helvetica Neue, Helvetica, Arial, sans-serif !important; + font-size: 14px !important; + line-height: 24px !important; + overflow: auto !important; + + height: 100% !important; + width: 100% !important; + + bottom: 0 !important; + left: 0 !important; + right: 0 !important; + top: 0 !important; + position: fixed !important; + z-index: 99999999 !important; +} + +.web-to-plex-prompt-body { + background: #282828 !important; + box-shadow: 0 5px 15px #0008 !important; + display: block !important; + + left: 20% !important; + top: 5% !important; + padding-top: 10px !important; + padding-bottom: 70px !important; + position: relative !important; + + height: 60% !important; + width: 60% !important; +} + +.web-to-plex-prompt-header, .web-to-plex-prompt-footer { + background: #323232 !important; + border: 1px solid #0000 !important; + box-sizing: border-box !important; + color: #eee !important; + font: inherit !important; + font-size: 2em !important; + line-height: initial !important; + text-size-adjust: 100% !important; + + margin-top: 0 !important; + padding: 15px 20px !important; + position: absolute !important; + + height: 65px !important; + width: 100% !important; + + -webkit-tap-highlight-color: #0000; +} + +.web-to-plex-prompt-header { + text-align: left !important; + border-bottom-color: #222 !important; + border-bottom-width: 1px !important; + border-top-left-radius: 3px !important; + border-top-right-radius: 3px !important; + + top: 0 !important; +} + +.web-to-plex-prompt-options { + display: block !important; + overflow-x: hidden !important; + overflow-y: auto !important; + + padding: 12px !important; + position: relative !important; + top: 65px !important; + + max-height: calc(100% - 65px) !important; +} + +.web-to-plex-prompt-option { + background: #323232 !important; + border: 1px solid #202020 !important; + border-radius: 3px !important; + color: #999 !important; + display: block !important; + text-align: left !important; + + margin-bottom: 10px !important; + padding: 10px !important; + + min-height: 20px !important; +} + +.web-to-plex-prompt-option.mutable { + max-width: 60% !important; +} + +.web-to-plex-prompt-option.mutable > h2 { + background: #0000 !important; + color: inherit !important; + font-family: inherit !important; + font-size: initial !important; + text-align: inherit !important; + + margin: inherit !important; +} + +.web-to-plex-prompt-option.mutable > .remove { + background: #ffffff40 !important; + border-radius: 3px !important; + transition: all 0.1s !important; + + height: 30px !important; + width: 30px !important; + + float: right !important; + margin-right: -9px !important; + margin-top: -42px !important; + padding: 0 !important; +} + +.web-to-plex-prompt-option.mutable > .remove:hover { + background: #ffffff4d !important; +} + +.web-to-plex-prompt-option.mutable > .remove::after { + content: '\00d7' !important; +} + +.web-to-plex-prompt-option.mutable > .quality { + width: 50% !important; +} + +.web-to-plex-prompt-option.mutable > .location { + width: 90% !important; +} + +.web-to-plex-prompt-option.mutable > .location:last-child:not(:first-child) { + margin-top: 5px !important; +} + +.web-to-plex-prompt-footer { + text-align: right !important; + border-bottom-left-radius: 3px !important; + border-bottom-right-radius: 3px !important; + border-top-color: #222 !important; + border-top-width: 1px !important; + + bottom: 0 !important; +} + +.web-to-plex-prompt-input { + float: left !important; + position: relative !important; + margin-left: -16px !important; + margin-top: -11px !important; +} + +.web-to-plex-prompt-accept, .web-to-plex-prompt-decline { + transition: all 0.1s !important; +} + +.web-to-plex-prompt-accept { + background: #cc7b19 !important; + margin-left: 5px !important; +} + +.web-to-plex-prompt-accept:hover { + background: #e59029 !important; +} + +.web-to-plex-prompt-decline { + background: #ffffff40 !important; +} + +.web-to-plex-prompt-decline:hover { + background: #ffffff4d !important; +} + +/* Web to Plex buttons */ +.web-to-plex-button [module] { + position: relative !important; +} + +.web-to-plex-button * { + border: none !important; +} + +.web-to-plex-button { + background-color: #3F4245 !important; + border: none !important; + color: #FFF !important; + font-family: Open Sans Semibold, Helvetica Neue, Helvetica, Arial, sans-serif !important; + font-size: 1em !important; + font-weight: 100 !important; + text-align: center !important; + + bottom: 5px !important; + left: 5px !important; + padding: 10px !important; + position: fixed !important; + right: unset !important; + z-index: 999999 !important; + + min-height: 0 !important; + min-width: 0 !important; + height: 72px !important; + width: 180px !important; + + transition: all 0.3s ease !important; + transition: opacity 1s ese !important; +} + +.web-to-plex-button.hide { + display: initial !important; +} + +.web-to-plex-button.hide:not(:hover), .web-to-plex-button.sleeper { + opacity: 0.1; +} + +*:not(#plexit-bookmarklet-frame) ~ .web-to-plex-button { + margin-left: 0px !important; +} + +#plexit-bookmarklet-frame ~ .web-to-plex-button { + margin-left: 280px !important; +} + +*:not(#plexit-bookmarklet-frame) + .web-to-plex-button #plexit, #plexit-bookmarklet-frame + .web-to-plex-button #wtp-plexit { + display: none !important; +} + +.floating.web-to-plex-button { + border-radius: 50px !important; + box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.3) !important; + + height: 75px !important; + width: 75px !important; +} +.floating.web-to-plex-button::after { + content: ' ' !important; + + background: #666 !important; + border: 1px solid #888 !important; + border-radius: 16px !important; + + right: 0 !important; + top: 0 !important; + position: absolute !important; + + height: 16px !important; + width: 16px !important; + + transition: background 0.4s linear !important; +} + +.floating.web-to-plex-button:not(.restarting):active, +.floating.web-to-plex-button:not(.restarting):hover { + box-shadow: 1px 5px 20px 0 rgba(0, 0, 0, 0.6) !important; + cursor: pointer !important; +} + +.floating.web-to-plex-button:focus { + outline: none !important; +} + +.web-to-plex-button.wtp--download::after, .web-to-plex-button.wtp--download::before { + background: #265AF4 !important; +} + +.web-to-plex-button.wtp--queued::after, .web-to-plex-button.wtp--queued::before { + background: #568AF4 !important; +} + +.web-to-plex-button.wtp--found::after, .web-to-plex-button.wtp--found::before { + background: #F9BD03 !important; +} + +.web-to-plex-button.wtp--error::after, .web-to-plex-button.wtp--error::before { + background: #FF2A2A !important; +} + +.web-to-plex-button::before { + content: ' ' !important; + + background: #FFF6 !important; + border-radius: inherit !important; + display: none !important; + + margin-top: -10px !important; + margin-left: -10px !important; + + height: 75px !important; + width: 75px !important; + + position: absolute !important; + z-index: 9999999 !important; +} + +.web-to-plex-button.animate::before { + display: block !important; + + -webkit-transform: scale(0); + -moz-transform: scale(0); + -o-transform: scale(0); + transform: scale(0); + + -webkit-animation: web-to-plex-ripple 0.5s linear; + -moz-animation: web-to-plex-ripple 0.5s linear; + -o-animation: web-to-plex-ripple 0.5s linear; + animation: web-to-plex-ripple 0.5s linear; +} + +@-webkit-keyframes web-to-plex-ripple { + 100% { + opacity: 0; + -webkit-transform: scale(2.5); + } +} +@-moz-keyframes web-to-plex-ripple { + 100% { + opacity: 0; + -moz-transform: scale(2.5); + } +} +@-o-keyframes web-to-plex-ripple { + 100% { + opacity: 0; + -o-transform: scale(2.5); + } +} +@keyframes web-to-plex-ripple { + 100% { + opacity: 0; + transform: scale(2.5); + } +} + +.web-to-plex-button.open, #plexit-bookmarklet-frame + .web-to-plex-button { + opacity: 1; + + width: 350px !important; +} +.web-to-plex-button .list-name { + float: left !important; +} + +.web-to-plex-button ul { + margin: 0 !important; + padding-left: 0 !important; +} + +.web-to-plex-button li { + display: inline-block !important; + list-style: none !important; + + margin: 0 !important; + padding: 5px !important; + vertical-align: bottom; +} + +.web-to-plex-button li > img { + display: inline !important; + + margin-top: 0 !important; +} + +*:not(#plexit-bookmarklet-frame) + .web-to-plex-button.closed .list-item { + float: left !important; + opacity: 0; + transition: opacity 0 !important; +} + +.web-to-plex-button.open .list-item { + opacity: 1; + transition: opacity 2s !important; +} + +.web-to-plex-button.open li:hover [tooltip]::before, .web-to-plex-button.open [tooltip]:hover::before { + content: attr(tooltip) !important; + + background: #3F424599 !important; + border-radius: 3px !important; + color: #fff !important; + font-family: arial, calibri, sans-serif, sans, monospace !important; + font-size: 15px !important; + + bottom: 85px !important; + left: 35px !important; + padding: 3px 6px !important; + position: absolute !important; +} + +/* bbodine @CodePen - https://codepen.io/bbodine1/pen/novBm */ +.checkbox { + width: 80px; + height: 26px; + background: #000; + margin: 15px 0; + position: relative; + border-radius: 50px; + box-shadow: inset 0px 1px 1px rgba(0, 0, 0, 0.5), 0px 1px 0px rgba(255, 255, 255, 0.2); +} + +span.checkbox { + display: inline-block; + + margin: 0; + vertical-align: text-bottom; +} + +.checkbox::after { + content: 'OFF'; + color: #666; + position: absolute; + right: 10px; + z-index: 0; + font: 12px/26px Arial, sans-serif; + font-weight: bold; + text-shadow: 1px 1px 0px rgba(255, 255, 255, 0.15); +} + +.checkbox::before { + content: 'ON'; + color: #cc7b19; + position: absolute; + left: 10px; + z-index: 0; + font: 12px/26px Arial, sans-serif; + font-weight: bold; +} + +.checkbox[prompt-yes]::before { + content: attr(prompt-yes); + text-transform: uppercase; +} + +.checkbox[prompt-no]::after { + content: attr(prompt-no); + text-transform: uppercase; +} + +.checkbox[prompt-size="large"i]::before, .checkbox[prompt-size="large"i]::after { + font-size: 30px !important; +} + +.checkbox[prompt-size="medium"i]::before, .checkbox[prompt-size="medium"i]::after { + font-size: 21px !important; +} + +.checkbox[prompt-size="normal"i]::before, .checkbox[prompt-size="normal"i]::after { + font-size: 12px !important; +} + +.checkbox[prompt-size="small"i]::before, .checkbox[prompt-size="small"i]::after { + font-size: 6px !important; +} + +.checkbox[prompt="y/n"i]::before { + content: 'YES'; +} + +.checkbox[prompt="y/n"i]::after { + content: 'NO'; +} + +.checkbox label { + display: block; + width: 34px; + height: 20px; + cursor: pointer; + position: absolute; + top: 3px; + left: 3px; + z-index: 1; + background: #666; + border-radius: 50px; + transition: all 0.4s ease; + box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.3); +} + +.checkbox input[type=checkbox] { + visibility: hidden; +} + +.checkbox input[type=checkbox]:checked + label { + left: 43px; + background: #cc7b19; +} + +.checkbox[disabled] { + opacity: 0.25 !important; +} diff --git a/moz/common.js b/moz/common.js new file mode 100644 index 0000000..bf8f6f4 --- /dev/null +++ b/moz/common.js @@ -0,0 +1,5 @@ +/* global Update(type:string, details:object) */ +if(init && typeof init == 'function') + /* Do nothing */; +else + (init = () => Update('PLUGIN', { instance_type: 'PLUGIN', plugin: location.hostname.replace(/(?:[\w\-]+\.)?([^\.]+)(?:\.[^\\\/]+)/, '$1') }))(); diff --git a/moz/consistent.js b/moz/consistent.js new file mode 100644 index 0000000..d688b5b --- /dev/null +++ b/moz/consistent.js @@ -0,0 +1,29 @@ +let NO_DEBUGGER = false; + +let terminal = + NO_DEBUGGER? + { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m }: + console; + +let check; + +check = document.body.onload = event => { + let video = document.querySelector('video'); + + if(video && (video.src || video.textContent)) { + let { src } = video; + + src = src || video.textContent; + + if(/^blob:/i.test(src)) + throw ' URL detected. Unable to reform file.'; + + try { + top.postMessage({ href: src, tail: 'MP4', type: 'SEND_VIDEO_LINK', from: 'consistent' }, '*'); + } catch(error) { + terminal.error('Failed to post message:', error); + } + } else { + setTimeout(check, 500); + } +}; diff --git a/moz/couchpotato$.js b/moz/couchpotato$.js new file mode 100644 index 0000000..7ef46cf --- /dev/null +++ b/moz/couchpotato$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'couchpotato' }))(); diff --git a/moz/couchpotato.js b/moz/couchpotato.js new file mode 100644 index 0000000..f8f6ddc --- /dev/null +++ b/moz/couchpotato.js @@ -0,0 +1,40 @@ +let script = { + "url": "*://*.couchpotato.life/(movies|shows)/*", + + "ready": () => !$('.media-body .clearfix').empty && $('.media-body .clearfix').first.children.length, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('[itemprop="description"]').first, + year = title.previousElementSibling, + image = $('img[src*="wp-content"]'), + type = script.getType(), + IMDbID = script.getIMDbID(); + + title = title.textContent.trim(); + year = +year.textContent.trim(); + image = image.empty? '': image.first.src; + + return { type, title, year, image, IMDbID }; + }, + + "getType": () => { + let pathname = window.location.pathname; + + return /^\/movies?\//.test(pathname)? + 'movie': + /^\/shows?\//.test(pathname)? + 'show': + null + }, + + "getIMDbID": () => { + let link = $('[href*="imdb.com/title/tt"]'); + + if(!link.empty) + return link.first.href + .replace(/^.*imdb\.com\/title\//, '') + .replace(/\/(?:maindetails\/?)?$/, ''); + }, +}; diff --git a/moz/couchpotato.png b/moz/couchpotato.png new file mode 100644 index 0000000..ed0b243 Binary files /dev/null and b/moz/couchpotato.png differ diff --git a/moz/example.png b/moz/example.png new file mode 100644 index 0000000..813ee11 Binary files /dev/null and b/moz/example.png differ diff --git a/moz/fandango$.js b/moz/fandango$.js new file mode 100644 index 0000000..366bb50 --- /dev/null +++ b/moz/fandango$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'fandango' }))(); diff --git a/moz/fandango.js b/moz/fandango.js new file mode 100644 index 0000000..04a1a4e --- /dev/null +++ b/moz/fandango.js @@ -0,0 +1,20 @@ +let script = { + "url": "*://*.fandango.com/[\\w\\-]+/movie-overview", + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('.subnav__title').first, + year = $('.movie-details__release-date').first, + image = $('.movie-details__movie-img').first, + type = 'movie'; + + title = title.textContent.trim().split(/\n+/)[0].trim(); + year = +year.textContent.replace(/.*(\d{4}).*/, '$1').trim(); + image = image.empty? '': image.src; + + title = title.replace(RegExp(`\\s*\\((${ year })\\)`), ''); + + return { type, title, year, image }; + }, +}; diff --git a/moz/fandango.png b/moz/fandango.png new file mode 100644 index 0000000..20a43db Binary files /dev/null and b/moz/fandango.png differ diff --git a/moz/flenix.png b/moz/flenix.png new file mode 100644 index 0000000..4e70583 Binary files /dev/null and b/moz/flenix.png differ diff --git a/moz/flickmetrix$.js b/moz/flickmetrix$.js new file mode 100644 index 0000000..4cbc2f3 --- /dev/null +++ b/moz/flickmetrix$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'flickmetrix' }))(); diff --git a/moz/flickmetrix.js b/moz/flickmetrix.js new file mode 100644 index 0000000..ab3a18a --- /dev/null +++ b/moz/flickmetrix.js @@ -0,0 +1,61 @@ +let script = { + "url": "*://*.flickmetrix.com/(watchlist|seen|favourites|trash|share|\\?)?", + + "ready": () => $('#loadingOverlay > *').empty, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + if(script.isList()) + return script.processList(ready); + + let element = $('#singleFilm'), type = 'movie'; + + _title = $('.title', film).first; + _year = $('.title + *', film).first; + _image = $('img', film).first; + + let title = _title.textContent.trim(), + year = +_year.textContent.replace(/^\(|\)$/g, '').trim(), + image = _image.src, + IMDbID = script.getIMDbID(element); + + return { type, title, year, image }; + }, + + "getIMDbID": (element) => { + let link = $('[href*="imdb.com/title/tt"]').first; + + if(link) + return link.href.replace(/^.*imdb\.com\/title\//, '').replace(/\/(?:maindetails\/?)?$/, ''); + }, + + "isList": () => $('#singleFilm').empty && !/\bid=\d+\b/i.test(location.search), + + "processList": (ready) => { + let _title, _year, _image, R = RegExp; + + let films = [], list = $('.film'), length = list.length - 1, type = 'movie'; + + list.forEach((element, index, array) => { + _title = $('.title', element).first; + _year = $('.title + *', element).first; + _image = $('img', element).first; + + if(!_title) + return; + + let title = _title.textContent.trim(), + year = +_year.textContent.replace(/^\(|\)$/g, '').trim(), + image = _image.src, + IMDbID = script.getIMDbID(element); + + films.push({ type, title, year, IMDbID }); + }); + + if(!films.length) + return new Notification('error', 'Failed to process list'); + + return films; + }, +}; diff --git a/moz/flickmetrix.png b/moz/flickmetrix.png new file mode 100644 index 0000000..51885b2 Binary files /dev/null and b/moz/flickmetrix.png differ diff --git a/moz/google$.js b/moz/google$.js new file mode 100644 index 0000000..1e3c09f --- /dev/null +++ b/moz/google$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'google' }))(); diff --git a/moz/google.js b/moz/google.js new file mode 100644 index 0000000..43ceb63 --- /dev/null +++ b/moz/google.js @@ -0,0 +1,61 @@ +let SHOW = '[href*="thetvdb.com/"][href*="id="], [href*="thetvdb.com/series/"], [href*="themoviedb.org/tv/"], [href*="imdb.com/title/tt"][href$="externalsites"]', + FILM = '[href*="themoviedb.org/tv/"], [href*="imdb.com/title/tt"]'; + // FILM = '#media_result_group, ...' + +let script = { + "url": "*://www.google.com/search", + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let type = script.getType(); + + if(type == 'movie') { + let _type = $('.kno-ecr-pt + *').first; // in case a tv show is incorrectly identified + + if(_type) { + type = _type.textContent; + + type = /\b(tv|show|series)\b/i.test(type)? 'show': /\b(movie|film|cinema|(?:\d+h\s+)?\d+m)\b/i.test(type)? 'movie': 'error'; + _year = (type == 'show'? $('.kno-fv').first || _year: _year) || { textContent: '' }; + } + + _title = $('.kno-ecr-pt').first; + _year = $('.kno-fb-ctx:not([data-local-attribute]) span').first; + _image = $('#media_result_group img').first; + } else if(type == 'show') { + _title = $(SHOW).first.querySelector('*'); + _year = { textContent: '' }; + _image = { src: '' }; + } else if(type == 'error') { + return null; + } + + (_year.textContent + '').replace(/(\d{4})/); + + let year = +R.$1, + title = _title.textContent.replace((type == 'movie'? /^(.+)$/: /(.+)(?:(?:\:\s*series\s+info|\-\s*(?:all\s+episodes|season)).+)$/i), '$1').trim(), + image = (_image || {}).src; + + year = year > 999? year: 0; + + let IMDbID = script.getIMDbID(); + + return { type, title, year, image, IMDbID }; + }, + + "getIMDbID": () => { + let link = $('a._hvg[href*="imdb.com/title/tt"]').first; + + if(link) + return link.href.replace(/.*(tt\d+).*/, '$1'); + }, + + "getType": () => ( + !$(SHOW).empty? + 'show': + !$(FILM).empty? + 'movie': + 'error' + ), +}; diff --git a/moz/google.play$.js b/moz/google.play$.js new file mode 100644 index 0000000..0d53cab --- /dev/null +++ b/moz/google.play$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'google.play' }))(); diff --git a/moz/google.play.js b/moz/google.play.js new file mode 100644 index 0000000..603e989 --- /dev/null +++ b/moz/google.play.js @@ -0,0 +1,24 @@ +let script = { + "url": "*://play.google.com/store/(movies|tv)/details/*", + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let type = script.getType(), + title = $('h1').first, + year = $(`h1 ~ div span:${ type == 'movie'? 'first': 'last' }-of-type`).first, + image = $('img[alt="cover art" i]').first; + + title = title.textContent.replace(/\s*\(\s*(\d{4})\s*\).*?$/, '').trim(); + year = +(year.textContent || R.$1).replace(/^.*?(\d{4})/, '$1').trim(); + image = (image || {}).src; + + return { type, title, year, image }; + }, + + "getType": () => ( + location.pathname.startsWith('/store/movies')? + 'movie': + 'show' + ), +}; diff --git a/moz/google.png b/moz/google.png new file mode 100644 index 0000000..d10c372 Binary files /dev/null and b/moz/google.png differ diff --git a/moz/gostream$.js b/moz/gostream$.js new file mode 100644 index 0000000..cc74b85 --- /dev/null +++ b/moz/gostream$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'gostream' }))(); diff --git a/moz/gostream.js b/moz/gostream.js new file mode 100644 index 0000000..1aaf12b --- /dev/null +++ b/moz/gostream.js @@ -0,0 +1,22 @@ +let script = { + "url": "*://*.gostream.site/(?!genre|most-viewed|top-imdb|contact)", + + "ready": () => { let e = $('.movieplay iframe, .desc iframe'); return e.empty? false: e.first.src != '' }, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('[itemprop="name"]:not(meta)').first, + year = $('.mvic-desc [href*="year/"]').first, + image = $('.hiddenz, [itemprop="image"]').first, + type = 'movie'; + + Notify('update', 'Select the OL/VH server'); + + title = title.textContent.trim(); + year = +(year? year.textContent.trim(): 0); + image = (image? image.src: null); + + return { type, title, year, image }; + }, +}; diff --git a/moz/gostream.png b/moz/gostream.png new file mode 100644 index 0000000..50f830e Binary files /dev/null and b/moz/gostream.png differ diff --git a/moz/helpers.js b/moz/helpers.js new file mode 100644 index 0000000..98b1e6b --- /dev/null +++ b/moz/helpers.js @@ -0,0 +1,58 @@ +async function load(name = '') { + if(!name) return; + + let HELPERS_STORAGE = chrome.storage.sync || chrome.storage.local; + + name = '~/cache/' + (name.toLowerCase().replace(/\s+/g, '_')); + + return new Promise((resolve, reject) => { + function LOAD(DISK) { + let data = JSON.parse(DISK[name] || null); + + return resolve(data); + } + + HELPERS_STORAGE.get(null, DISK => { + if(chrome.runtime.lastError) + chrome.runtime.lastError.message || + chrome.storage.local.get(null, LOAD); + else + LOAD(DISK); + }); + }); +} + +async function save(name = '', data) { + if(!name) return; + + let HELPERS_STORAGE = chrome.storage.sync || chrome.storage.local; + + name = '~/cache/' + (name.toLowerCase().replace(/\s+/g, '_')); + data = JSON.stringify(data); + + await HELPERS_STORAGE.set({[name]: data}, () => data); + + return name; +} + +async function kill(name) { + let HELPERS_STORAGE = chrome.storage.sync || chrome.storage.local; + + return HELPERS_STORAGE.remove(['~/cache/' + (name.toLowerCase().replace(/\s+/g, '_'))]); +} + +async function Notify(state, text, timeout = 7000, requiresClick = true) { + return top.postMessage({ type: 'NOTIFICATION', data: { state, text, timeout, requiresClick } }, '*'); +} + +async function Require(permission, name, alias) { + let allowed = await load(`has/${ name }`), + allotted = await load(`get/${ name }`); + + top.postMessage({ type: 'PERMISSION', data: { permission, name, alias, allowed, allotted } }); + + /* Already asked for permission */ + if(typeof allowed == 'boolean') + /* The allowed permission(s) */ + return allotted; +} diff --git a/moz/hide.16.png b/moz/hide.16.png new file mode 100644 index 0000000..2e17942 Binary files /dev/null and b/moz/hide.16.png differ diff --git a/moz/hide.48.png b/moz/hide.48.png new file mode 100644 index 0000000..b7ec4ed Binary files /dev/null and b/moz/hide.48.png differ diff --git a/moz/history-hack.js b/moz/history-hack.js new file mode 100644 index 0000000..4fb99a3 --- /dev/null +++ b/moz/history-hack.js @@ -0,0 +1,26 @@ +let __script__ = document.createElement('script'); + +// Injected DOM script is not a content script anymore; +// It can modify objects and functions of the page +__script__.text = `(${ +function() { + let history = window.history, + __pushState__ = history.pushState, + __replaceState__ = history.replaceState; + + history.pushState = function(state, title, url) { + __pushState__.call(this, state, title, url); + + window.dispatchEvent(new CustomEvent('pushstate-changed', { detail: state })); + }; + + history.replaceState = function(state, title, url) { + __replaceState__.call(this, state, title, url); + + window.dispatchEvent(new CustomEvent('pushstate-changed', { detail: state })); + }; +} +})();`; + +document.head.appendChild(__script__); +document.head.removeChild(__script__); diff --git a/moz/hulu$.js b/moz/hulu$.js new file mode 100644 index 0000000..9b777ed --- /dev/null +++ b/moz/hulu$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'hulu' }))(); diff --git a/moz/hulu.js b/moz/hulu.js new file mode 100644 index 0000000..89a8081 --- /dev/null +++ b/moz/hulu.js @@ -0,0 +1,48 @@ +let script = { + "url": "*://*.hulu.com/(watch|series|movie)/*", + + "ready": () => !$('[class$="__meta"]').empty, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + let { pathname } = top.location; + let type, title, year, image; + + if(/^\/(series|movie)\//.test(pathname)) { + type = R.$1; + title = $('[class~="masthead__title"i]').first; + year = $('[class~="masthead__meta"i]').child(type == 'series'? 4: 3); + image = $('[class~="masthead__artwork"i]').first; + + title = title.textContent; + year = +year.textContent; + type = /\b(tv|show|season|series)\b/i.test(type)? 'show': 'movie'; + image = image? image.src: null; + } else { + title = $('[class$="__second-line"]').first; + year = (new Date).getFullYear(); + type = script.getType(); + + title = title.textContent; + } + + if(!title) + return 5000; + + return { type, title, year, image }; + }, + + "getType": () => { + let { pathname } = top.location; + + if(/^\/series\//.test(pathname)) { + return 'show'; + } else { + let tl = $('[class$="__third-line"]').first; + + return /^\s*$/.test(tl.textContent)? + 'movie': + 'show'; + } + }, +}; diff --git a/moz/hulu.png b/moz/hulu.png new file mode 100644 index 0000000..bc5a080 Binary files /dev/null and b/moz/hulu.png differ diff --git a/moz/imdb$.js b/moz/imdb$.js new file mode 100644 index 0000000..1c4ef9a --- /dev/null +++ b/moz/imdb$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'imdb' }))(); diff --git a/moz/imdb.js b/moz/imdb.js new file mode 100644 index 0000000..3e8a624 --- /dev/null +++ b/moz/imdb.js @@ -0,0 +1,119 @@ +let script = { + "url": "*://*.imdb.com/(title|list)/(tt|ls)\\d+/", + + "ready": () => !$('#servertime').empty, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let type = script.getType(), + IMDbID = script.getIMDbID(), + title, year, image; + + let usa = /\b(USA?|United\s+States)\b/i, + date, country, reldate, regdate, alttitle, options; + + switch(type) { + case 'movie': + title = $('.originalTitle, .title_wrapper h1'); + alttitle = title.first; + reldate = $('.title_wrapper [href*="/releaseinfo"]').first; + year = $('.title_wrapper #titleYear').first; + image = $('img[alt$="poster"i]').first; + + // TODO: Less risky way to accompilsh this? + title = title.last.childNodes[0].textContent.trim(); + alttitle = (alttitle == title? title: alttitle.childNodes[0].textContent.trim()); + title = usa.test(country)? title: alttitle; + country = reldate.textContent.replace(/[^]+\((\w+)\)[^]*?$/, '$1'); + year = +script.clean(year.textContent); + image = (image || {}).src; + options = { type, title, alttitle, year, image }; + break; + + case 'show': + title = $('.originalTitle, .title_wrapper h1'); + alttitle = title.first; + reldate = $('.title_wrapper [href*="/releaseinfo"]').first; + date = $('title').first.textContent.trim(); + regdate = date.match(/Series\s*\(?(\d{4})(?:[^\)]+\))?/i); + image = $('img[alt$="poster"i]').first; + + // TODO: Less risky way to accompilsh this? + title = title.last.textContent.trim(); + alttitle = (alttitle == title? title: alttitle.childNodes[0].textContent.trim()); + title = usa.test(country)? title: alttitle; + country = reldate.textContent.replace(/[^]+\((\w+)\)[^]*?$/, '$1'); + year = parseInt(regdate[1]); + image = (image || {}).src; + options = { type, title, alttitle, year, image }; + break; + + case 'list': + let items = $('#main .lister-item'); + + options = []; + + if(!/[\?\&]mode=simple\b/i.test(top.location.search)) + top.open(location.href.replace(/([\?\&]|\/$)(?:mode=\w+&*)?/, '$1mode=simple&'), '_self'); + + items.forEach(element => { + let option = script.process(element); + + if(option) + options.push(option); + }); + break; + + default: return null; + } + + return options; + }, + + "getType": () => { + let tag = $('meta[property="og:type"]').first, + type = 'error'; + + if(tag) { + switch(tag.content) { + case 'video.movie': + type = 'movie'; + break; + + case 'video.tv_show': + type = 'show'; + break; + }; + } else if(top.location.pathname.startsWith('/list/')) { + type = 'list'; + } + + return type; + }, + + "getIMDbID": () => { + let tag = $('meta[property="pageId"]'); + + return tag? tag.content: null; + }, + + "process": (element) => { + let title = $('.col-title a', element).first, + year = $('.col-title a + *', element).first, + image = $('img.loadlate, img[data-tconst]', element).first, + IMDbID = title.href.replace(/^[^]*\/(tt\d+)\b[^]*$/, '$1'), + type; + + title = title.textContent.trim(); + year = script.clean(year.textContent); + image = image.src; + type = (/[\-\u2010-\u2015]/.test(year)? 'show': 'movie'); + + year = +year; + + return { type, title, year, image, IMDbID }; + }, + + "clean": year => (year + '').replace(/^\(|\)$/g, '').trim(), +}; diff --git a/moz/imdb.png b/moz/imdb.png new file mode 100644 index 0000000..d12a985 Binary files /dev/null and b/moz/imdb.png differ diff --git a/moz/itunes$.js b/moz/itunes$.js new file mode 100644 index 0000000..4f9ec1b --- /dev/null +++ b/moz/itunes$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'itunes' }))(); diff --git a/moz/itunes.js b/moz/itunes.js new file mode 100644 index 0000000..45f4cc8 --- /dev/null +++ b/moz/itunes.js @@ -0,0 +1,47 @@ +let script = { + "url": "", + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title, year, image, type = script.getType(); + + switch(type) { + case 'movie': + title = $('[class*="movie-header__title"i]').first.textContent; + year = +$('[datetime]').first.textContent || title.replace(RegExp(`[^]*\\((${ year })\\)[^]*`), '$1'); + image = ($('[class*="product"] ~ * picture img').first || {}).src; + + title = title.replace(RegExp(`\\s*\\(${ year }\\)`), ''); + break; + + case 'tv': + title = $('h1[itemprop="name"], h1').first.textContent.replace(/\s*\((\d+)\)\s*/, '').trim(); + year = +$('.release-date > *:last-child').first.textContent.replace(/[^]*(\d{4})[^]*?$/g, '$1').trim(); + image = $('[class*="product"] ~ * picture img').first.src; + + title = title.replace(RegExp(`\\s*\\(${ year }\\)`), ''); + break; + + default: + /* Error */ + return {}; + } + + setTimeout(script.adjustButton, 1000); + + return { type, title, year, image }; + }, + + "getType": () => { + return /(\/\w+)?\/tv-season\//.test(top.location.pathname)? + 'tv': + 'movie' + }, + + "adjustButton": () => { + let button = $('.web-to-plex-button').first; + + button.attributes.style.value += '; box-sizing: border-box !important; font-size: 16px !important; line-height: normal !important;'; + }, +}; diff --git a/moz/itunes.png b/moz/itunes.png new file mode 100644 index 0000000..7cbd8cd Binary files /dev/null and b/moz/itunes.png differ diff --git a/moz/justwatch$.js b/moz/justwatch$.js new file mode 100644 index 0000000..b5dff44 --- /dev/null +++ b/moz/justwatch$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'justwatch' }))(); diff --git a/moz/justwatch.js b/moz/justwatch.js new file mode 100644 index 0000000..35cedeb --- /dev/null +++ b/moz/justwatch.js @@ -0,0 +1,31 @@ +let script = { + "url": "*://*.justwatch.com/(\\w{2})/(tv(?:-show)|movie)/*", + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('.title-block').first, + year = $('.title-block .text-muted').first, + image = $('.title-poster__image').first, + type = script.getType(); + + if(!title || !year) + return 1000; + + year = year.textContent; + title = title.firstElementChild.firstChild.textContent.trim(); + year = +year.replace(/\D+/g, ''); + image = image.src; + + return { type, title, year, image }; + }, + + "getType": () => { + let { pathname } = top.location; + + if(/^\/tv(-show)?\//.test(pathname)) + return 'show'; + else + return 'movie'; + }, +}; diff --git a/moz/justwatch.png b/moz/justwatch.png new file mode 100644 index 0000000..7f4bf1f Binary files /dev/null and b/moz/justwatch.png differ diff --git a/moz/letterboxd$.js b/moz/letterboxd$.js new file mode 100644 index 0000000..80cd852 --- /dev/null +++ b/moz/letterboxd$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'letterboxd' }))(); diff --git a/moz/letterboxd.js b/moz/letterboxd.js new file mode 100644 index 0000000..d8c1ac7 --- /dev/null +++ b/moz/letterboxd.js @@ -0,0 +1,76 @@ +let script = { + "url": "*://*.letterboxd.com/(film|list)/", + + "ready": () => (script.getType('list')? true: !$('.js-watch-panel').empty), + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title, year, image, type = script.getType(), IMDbID; + + switch(type) { + case 'movie': + title = $('.headline-1[itemprop="name"]').first.textContent.trim(); + year = +$('small[itemprop="datePublished"]').first.textContent.trim(); + image = ($('.image').first || {}).src; + IMDbID = script.getIMDbID(type); + + return { type, title, year, image, IMDbID }; + break; + + case 'list': + let items = $('.poster-list .poster-container'), + options = []; + + items.forEach((element, index, array) => { + let option = script.process(element); + + if(option) + options.push(option); + }); + + return options; + break; + + default: + /* Error */ + return {}; + } + }, + + "getType": (suspectedType) => { + let type = /^\/(film)\//i.test(top.location.pathname)? 'movie': 'list'; + + if(suspectedType) + return type == suspectedType; + + return type; + }, + + "getIMDbID": (type) => { + if(type == 'movie') { + let link = $( + '.track-event[href*="imdb.com/title/tt"i]' + ); + + if(!link.empty) { + link = link.first.href.replace(/^.*imdb\.com\/title\//i, ''); + + return link.replace(/\/(?:maindetails\/?)?$/, ''); + } + } + }, + + "process": (element) => { + let title = $('.frame-title', element).first, + image = $('img', element).first, + type = 'movie', + year; + + title = title.textContent.replace(/\((\d+)\)/, '').trim(); + year = +RegExp.$1; + image = image.src; + + return { type, title, year, image }; + }, +}; diff --git a/moz/letterboxd.png b/moz/letterboxd.png new file mode 100644 index 0000000..b16eae3 Binary files /dev/null and b/moz/letterboxd.png differ diff --git a/moz/loading.png b/moz/loading.png new file mode 100644 index 0000000..749aa93 Binary files /dev/null and b/moz/loading.png differ diff --git a/moz/local.couchpotato.png b/moz/local.couchpotato.png new file mode 100644 index 0000000..a53d3eb Binary files /dev/null and b/moz/local.couchpotato.png differ diff --git a/moz/local.medusa.png b/moz/local.medusa.png new file mode 100644 index 0000000..36e775d Binary files /dev/null and b/moz/local.medusa.png differ diff --git a/moz/local.ombi.png b/moz/local.ombi.png new file mode 100644 index 0000000..0c9c24d Binary files /dev/null and b/moz/local.ombi.png differ diff --git a/moz/local.plex.png b/moz/local.plex.png new file mode 100644 index 0000000..75ea659 Binary files /dev/null and b/moz/local.plex.png differ diff --git a/moz/local.radarr.png b/moz/local.radarr.png new file mode 100644 index 0000000..f55ef6b Binary files /dev/null and b/moz/local.radarr.png differ diff --git a/moz/local.sickBeard.png b/moz/local.sickBeard.png new file mode 100644 index 0000000..bc16cb3 Binary files /dev/null and b/moz/local.sickBeard.png differ diff --git a/moz/local.sonarr.png b/moz/local.sonarr.png new file mode 100644 index 0000000..93585db Binary files /dev/null and b/moz/local.sonarr.png differ diff --git a/moz/local.watcher.png b/moz/local.watcher.png new file mode 100644 index 0000000..581d81b Binary files /dev/null and b/moz/local.watcher.png differ diff --git a/moz/lodash.min.js b/moz/lodash.min.js new file mode 100644 index 0000000..a3c7dac --- /dev/null +++ b/moz/lodash.min.js @@ -0,0 +1,5 @@ +/* eslint-disable */ +// lodash 4.17.2 +(function(){function n(n,t){return n.set(t[0],t[1]),n}function t(n,t){return n.add(t),n}function r(n,t,r){switch(r.length){case 0:return n.call(t);case 1:return n.call(t,r[0]);case 2:return n.call(t,r[0],r[1]);case 3:return n.call(t,r[0],r[1],r[2])}return n.apply(t,r)}function e(n,t,r,e){for(var u=-1,i=null==n?0:n.length;++u-1}function c(n,t,r){for(var e=-1,u=null==n?0:n.length;++e-1;);return r}function C(n,t){for(var r=n.length;r--&&b(t,n[r],0)>-1;);return r}function U(n,t){for(var r=n.length,e=0;r--;)n[r]===t&&++e;return e}function B(n){return"\\"+Yr[n]}function T(n,t){return null==n?X:n[t]}function $(n){return Nr.test(n)}function D(n){return Pr.test(n)}function M(n){for(var t,r=[];!(t=n.next()).done;)r.push(t.value);return r}function F(n){var t=-1,r=Array(n.size);return n.forEach(function(n,e){r[++t]=[e,n]}),r}function N(n,t){return function(r){return n(t(r))}}function P(n,t){for(var r=-1,e=n.length,u=0,i=[];++r>>1,Tn=[["ary",wn],["bind",pn],["bindKey",vn],["curry",gn],["curryRight",yn],["flip",xn],["partial",dn],["partialRight",bn],["rearg",mn]],$n="[object Arguments]",Dn="[object Array]",Mn="[object AsyncFunction]",Fn="[object Boolean]",Nn="[object Date]",Pn="[object DOMException]",qn="[object Error]",Zn="[object Function]",Kn="[object GeneratorFunction]",Vn="[object Map]",Gn="[object Number]",Hn="[object Null]",Jn="[object Object]",Yn="[object Promise]",Qn="[object Proxy]",Xn="[object RegExp]",nt="[object Set]",tt="[object String]",rt="[object Symbol]",et="[object Undefined]",ut="[object WeakMap]",it="[object WeakSet]",ot="[object ArrayBuffer]",ft="[object DataView]",at="[object Float32Array]",ct="[object Float64Array]",lt="[object Int8Array]",st="[object Int16Array]",ht="[object Int32Array]",pt="[object Uint8Array]",vt="[object Uint8ClampedArray]",_t="[object Uint16Array]",gt="[object Uint32Array]",yt=/\b__p \+= '';/g,dt=/\b(__p \+=) '' \+/g,bt=/(__e\(.*?\)|\b__t\)) \+\n'';/g,wt=/&(?:amp|lt|gt|quot|#39);/g,mt=/[&<>"']/g,xt=RegExp(wt.source),jt=RegExp(mt.source),At=/<%-([\s\S]+?)%>/g,kt=/<%([\s\S]+?)%>/g,Ot=/<%=([\s\S]+?)%>/g,It=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,Rt=/^\w*$/,zt=/^\./,Et=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,St=/[\\^$.*+?()[\]{}|]/g,Wt=RegExp(St.source),Lt=/^\s+|\s+$/g,Ct=/^\s+/,Ut=/\s+$/,Bt=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,Tt=/\{\n\/\* \[wrapped with (.+)\] \*/,$t=/,? & /,Dt=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g,Mt=/\\(\\)?/g,Ft=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,Nt=/\w*$/,Pt=/^[-+]0x[0-9a-f]+$/i,qt=/^0b[01]+$/i,Zt=/^\[object .+?Constructor\]$/,Kt=/^0o[0-7]+$/i,Vt=/^(?:0|[1-9]\d*)$/,Gt=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,Ht=/($^)/,Jt=/['\n\r\u2028\u2029\\]/g,Yt="\\ud800-\\udfff",Qt="\\u0300-\\u036f",Xt="\\ufe20-\\ufe2f",nr="\\u20d0-\\u20ff",tr=Qt+Xt+nr,rr="\\u2700-\\u27bf",er="a-z\\xdf-\\xf6\\xf8-\\xff",ur="\\xac\\xb1\\xd7\\xf7",ir="\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf",or="\\u2000-\\u206f",fr=" \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",ar="A-Z\\xc0-\\xd6\\xd8-\\xde",cr="\\ufe0e\\ufe0f",lr=ur+ir+or+fr,sr="['’]",hr="["+Yt+"]",pr="["+lr+"]",vr="["+tr+"]",_r="\\d+",gr="["+rr+"]",yr="["+er+"]",dr="[^"+Yt+lr+_r+rr+er+ar+"]",br="\\ud83c[\\udffb-\\udfff]",wr="(?:"+vr+"|"+br+")",mr="[^"+Yt+"]",xr="(?:\\ud83c[\\udde6-\\uddff]){2}",jr="[\\ud800-\\udbff][\\udc00-\\udfff]",Ar="["+ar+"]",kr="\\u200d",Or="(?:"+yr+"|"+dr+")",Ir="(?:"+Ar+"|"+dr+")",Rr="(?:"+sr+"(?:d|ll|m|re|s|t|ve))?",zr="(?:"+sr+"(?:D|LL|M|RE|S|T|VE))?",Er=wr+"?",Sr="["+cr+"]?",Wr="(?:"+kr+"(?:"+[mr,xr,jr].join("|")+")"+Sr+Er+")*",Lr="\\d*(?:(?:1st|2nd|3rd|(?![123])\\dth)\\b)",Cr="\\d*(?:(?:1ST|2ND|3RD|(?![123])\\dTH)\\b)",Ur=Sr+Er+Wr,Br="(?:"+[gr,xr,jr].join("|")+")"+Ur,Tr="(?:"+[mr+vr+"?",vr,xr,jr,hr].join("|")+")",$r=RegExp(sr,"g"),Dr=RegExp(vr,"g"),Mr=RegExp(br+"(?="+br+")|"+Tr+Ur,"g"),Fr=RegExp([Ar+"?"+yr+"+"+Rr+"(?="+[pr,Ar,"$"].join("|")+")",Ir+"+"+zr+"(?="+[pr,Ar+Or,"$"].join("|")+")",Ar+"?"+Or+"+"+Rr,Ar+"+"+zr,Cr,Lr,_r,Br].join("|"),"g"),Nr=RegExp("["+kr+Yt+tr+cr+"]"),Pr=/[a-z][A-Z]|[A-Z]{2,}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/,qr=["Array","Buffer","DataView","Date","Error","Float32Array","Float64Array","Function","Int8Array","Int16Array","Int32Array","Map","Math","Object","Promise","RegExp","Set","String","Symbol","TypeError","Uint8Array","Uint8ClampedArray","Uint16Array","Uint32Array","WeakMap","_","clearTimeout","isFinite","parseInt","setTimeout"],Zr=-1,Kr={};Kr[at]=Kr[ct]=Kr[lt]=Kr[st]=Kr[ht]=Kr[pt]=Kr[vt]=Kr[_t]=Kr[gt]=!0,Kr[$n]=Kr[Dn]=Kr[ot]=Kr[Fn]=Kr[ft]=Kr[Nn]=Kr[qn]=Kr[Zn]=Kr[Vn]=Kr[Gn]=Kr[Jn]=Kr[Xn]=Kr[nt]=Kr[tt]=Kr[ut]=!1;var Vr={};Vr[$n]=Vr[Dn]=Vr[ot]=Vr[ft]=Vr[Fn]=Vr[Nn]=Vr[at]=Vr[ct]=Vr[lt]=Vr[st]=Vr[ht]=Vr[Vn]=Vr[Gn]=Vr[Jn]=Vr[Xn]=Vr[nt]=Vr[tt]=Vr[rt]=Vr[pt]=Vr[vt]=Vr[_t]=Vr[gt]=!0,Vr[qn]=Vr[Zn]=Vr[ut]=!1;var Gr={"À":"A","Á":"A","Â":"A","Ã":"A","Ä":"A","Å":"A","à":"a","á":"a","â":"a","ã":"a","ä":"a","å":"a","Ç":"C","ç":"c","Ð":"D","ð":"d","È":"E","É":"E","Ê":"E","Ë":"E","è":"e","é":"e","ê":"e","ë":"e","Ì":"I","Í":"I","Î":"I","Ï":"I","ì":"i","í":"i","î":"i","ï":"i","Ñ":"N","ñ":"n","Ò":"O","Ó":"O","Ô":"O","Õ":"O","Ö":"O","Ø":"O","ò":"o","ó":"o","ô":"o","õ":"o","ö":"o","ø":"o","Ù":"U","Ú":"U","Û":"U","Ü":"U","ù":"u","ú":"u","û":"u","ü":"u","Ý":"Y","ý":"y","ÿ":"y","Æ":"Ae","æ":"ae","Þ":"Th","þ":"th","ß":"ss","Ā":"A","Ă":"A","Ą":"A","ā":"a","ă":"a","ą":"a","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","ć":"c","ĉ":"c","ċ":"c","č":"c","Ď":"D","Đ":"D","ď":"d","đ":"d","Ē":"E","Ĕ":"E","Ė":"E","Ę":"E","Ě":"E","ē":"e","ĕ":"e","ė":"e","ę":"e","ě":"e","Ĝ":"G","Ğ":"G","Ġ":"G","Ģ":"G","ĝ":"g","ğ":"g","ġ":"g","ģ":"g","Ĥ":"H","Ħ":"H","ĥ":"h","ħ":"h","Ĩ":"I","Ī":"I","Ĭ":"I","Į":"I","İ":"I","ĩ":"i","ī":"i","ĭ":"i","į":"i","ı":"i","Ĵ":"J","ĵ":"j","Ķ":"K","ķ":"k","ĸ":"k","Ĺ":"L","Ļ":"L","Ľ":"L","Ŀ":"L","Ł":"L","ĺ":"l","ļ":"l","ľ":"l","ŀ":"l","ł":"l","Ń":"N","Ņ":"N","Ň":"N","Ŋ":"N","ń":"n","ņ":"n","ň":"n","ŋ":"n","Ō":"O","Ŏ":"O","Ő":"O","ō":"o","ŏ":"o","ő":"o","Ŕ":"R","Ŗ":"R","Ř":"R","ŕ":"r","ŗ":"r","ř":"r","Ś":"S","Ŝ":"S","Ş":"S","Š":"S","ś":"s","ŝ":"s","ş":"s","š":"s","Ţ":"T","Ť":"T","Ŧ":"T","ţ":"t","ť":"t","ŧ":"t","Ũ":"U","Ū":"U","Ŭ":"U","Ů":"U","Ű":"U","Ų":"U","ũ":"u","ū":"u","ŭ":"u","ů":"u","ű":"u","ų":"u","Ŵ":"W","ŵ":"w","Ŷ":"Y","ŷ":"y","Ÿ":"Y","Ź":"Z","Ż":"Z","Ž":"Z","ź":"z","ż":"z","ž":"z","IJ":"IJ","ij":"ij","Œ":"Oe","œ":"oe","ʼn":"'n","ſ":"s"},Hr={"&":"&","<":"<",">":">",'"':""","'":"'"},Jr={"&":"&","<":"<",">":">",""":'"',"'":"'"},Yr={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},Qr=parseFloat,Xr=parseInt,ne="object"==typeof global&&global&&global.Object===Object&&global,te="object"==typeof self&&self&&self.Object===Object&&self,re=ne||te||Function("return this")(),ee="object"==typeof exports&&exports&&!exports.nodeType&&exports,ue=ee&&"object"==typeof module&&module&&!module.nodeType&&module,ie=ue&&ue.exports===ee,oe=ie&&ne.process,fe=function(){try{return oe&&oe.binding&&oe.binding("util")}catch(n){}}(),ae=fe&&fe.isArrayBuffer,ce=fe&&fe.isDate,le=fe&&fe.isMap,se=fe&&fe.isRegExp,he=fe&&fe.isSet,pe=fe&&fe.isTypedArray,ve=j("length"),_e=A(Gr),ge=A(Hr),ye=A(Jr),de=function _(A){function K(n){if(ca(n)&&!wh(n)&&!(n instanceof Dt)){if(n instanceof Y)return n;if(wl.call(n,"__wrapped__"))return uo(n)}return new Y(n)}function J(){}function Y(n,t){this.__wrapped__=n,this.__actions__=[],this.__chain__=!!t,this.__index__=0,this.__values__=X}function Dt(n){this.__wrapped__=n,this.__actions__=[],this.__dir__=1,this.__filtered__=!1,this.__iteratees__=[],this.__takeCount__=Cn,this.__views__=[]}function Yt(){var n=new Dt(this.__wrapped__);return n.__actions__=Fu(this.__actions__),n.__dir__=this.__dir__,n.__filtered__=this.__filtered__,n.__iteratees__=Fu(this.__iteratees__),n.__takeCount__=this.__takeCount__,n.__views__=Fu(this.__views__),n}function Qt(){if(this.__filtered__){var n=new Dt(this);n.__dir__=-1,n.__filtered__=!0}else n=this.clone(),n.__dir__*=-1;return n}function Xt(){var n=this.__wrapped__.value(),t=this.__dir__,r=wh(n),e=t<0,u=r?n.length:0,i=Ii(0,u,this.__views__),o=i.start,f=i.end,a=f-o,c=e?f:o-1,l=this.__iteratees__,s=l.length,h=0,p=Jl(a,this.__takeCount__);if(!r||u-1}function sr(n,t){var r=this.__data__,e=Cr(r,n);return e<0?(++this.size,r.push([n,t])):r[e][1]=t,this}function hr(n){var t=-1,r=null==n?0:n.length;for(this.clear();++t=t?n:t)),n}function Pr(n,t,r,e,i,o){var f,a=t&an,c=t&cn,l=t&ln;if(r&&(f=i?r(n,e,i,o):r(n)),f!==X)return f;if(!aa(n))return n;var s=wh(n);if(s){if(f=Ei(n),!a)return Fu(n,f)}else{var h=Es(n),p=h==Zn||h==Kn;if(xh(n))return zu(n,a);if(h==Jn||h==$n||p&&!i){if(f=c||p?{}:Si(n),!a)return c?qu(n,Tr(f,n)):Pu(n,Br(f,n))}else{if(!Vr[h])return i?n:{};f=Wi(n,h,Pr,a)}}o||(o=new mr);var v=o.get(n);if(v)return v;o.set(n,f);var _=l?c?bi:di:c?Za:qa,g=s?X:_(n);return u(g||n,function(e,u){g&&(u=e,e=n[u]),Lr(f,u,Pr(e,t,r,u,n,o))}),f}function Gr(n){var t=qa(n);return function(r){return Hr(r,n,t)}}function Hr(n,t,r){var e=r.length;if(null==n)return!e;for(n=sl(n);e--;){var u=r[e],i=t[u],o=n[u];if(o===X&&!(u in n)||!i(o))return!1}return!0}function Jr(n,t,r){if("function"!=typeof n)throw new vl(en);return Ls(function(){n.apply(X,r)},t)}function Yr(n,t,r,e){var u=-1,i=a,o=!0,f=n.length,s=[],h=t.length;if(!f)return s;r&&(t=l(t,E(r))),e?(i=c,o=!1):t.length>=tn&&(i=W,o=!1,t=new dr(t));n:for(;++uu?0:u+r),e=e===X||e>u?u:Oa(e),e<0&&(e+=u),e=r>e?0:Ia(e);r0&&r(f)?t>1?oe(f,t-1,r,e,u):s(u,f):e||(u[u.length]=f)}return u}function fe(n,t){return n&&ws(n,t,qa)}function ve(n,t){return n&&ms(n,t,qa)}function de(n,t){return f(t,function(t){return ia(n[t])})}function we(n,t){t=Iu(t,n);for(var r=0,e=t.length;null!=n&&rt}function Ae(n,t){return null!=n&&wl.call(n,t)}function ke(n,t){return null!=n&&t in sl(n)}function Oe(n,t,r){return n>=Jl(t,r)&&n=120&&p.length>=120)?new dr(o&&p):X}p=n[0];var v=-1,_=f[0];n:for(;++v-1;)f!==n&&Cl.call(f,a,1),Cl.call(n,a,1);return n}function ru(n,t){for(var r=n?t.length:0,e=r-1;r--;){var u=t[r];if(r==e||u!==i){var i=u;Ui(u)?Cl.call(n,u,1):bu(n,u)}}return n}function eu(n,t){return n+Pl(Xl()*(t-n+1))}function uu(n,t,r,e){for(var u=-1,i=Hl(Nl((t-n)/(r||1)),0),o=ol(i);i--;)o[e?i:++u]=n,n+=r;return o}function iu(n,t){var r="";if(!n||t<1||t>Sn)return r;do t%2&&(r+=n),t=Pl(t/2),t&&(n+=n);while(t);return r}function ou(n,t){return Cs(Hi(n,t,Cc),n+"")}function fu(n){return Rr(ec(n))}function au(n,t){var r=ec(n);return no(r,Nr(t,0,r.length))}function cu(n,t,r,e){if(!aa(n))return n;t=Iu(t,n);for(var u=-1,i=t.length,o=i-1,f=n;null!=f&&++uu?0:u+t),r=r>u?u:r,r<0&&(r+=u),u=t>r?0:r-t>>>0,t>>>=0;for(var i=ol(u);++e>>1,o=n[i];null!==o&&!wa(o)&&(r?o<=t:o=tn){var s=t?null:Os(n);if(s)return q(s);o=!1,u=W,l=new dr}else l=t?[]:f;n:for(;++e=e?n:su(n,t,r)}function zu(n,t){if(t)return n.slice();var r=n.length,e=El?El(r):new n.constructor(r);return n.copy(e),e}function Eu(n){var t=new n.constructor(n.byteLength);return new zl(t).set(new zl(n)),t}function Su(n,t){var r=t?Eu(n.buffer):n.buffer;return new n.constructor(r,n.byteOffset,n.byteLength)}function Wu(t,r,e){var u=r?e(F(t),an):F(t);return h(u,n,new t.constructor)}function Lu(n){var t=new n.constructor(n.source,Nt.exec(n));return t.lastIndex=n.lastIndex,t}function Cu(n,r,e){var u=r?e(q(n),an):q(n);return h(u,t,new n.constructor)}function Uu(n){return _s?sl(_s.call(n)):{}}function Bu(n,t){var r=t?Eu(n.buffer):n.buffer;return new n.constructor(r,n.byteOffset,n.length)}function Tu(n,t){if(n!==t){var r=n!==X,e=null===n,u=n===n,i=wa(n),o=t!==X,f=null===t,a=t===t,c=wa(t);if(!f&&!c&&!i&&n>t||i&&o&&a&&!f&&!c||e&&o&&a||!r&&a||!u)return 1;if(!e&&!i&&!c&&n=f)return a;var c=r[e];return a*("desc"==c?-1:1)}}return n.index-t.index}function Du(n,t,r,e){for(var u=-1,i=n.length,o=r.length,f=-1,a=t.length,c=Hl(i-o,0),l=ol(a+c),s=!e;++f1?r[u-1]:X,o=u>2?r[2]:X;for(i=n.length>3&&"function"==typeof i?(u--,i):X,o&&Bi(r[0],r[1],o)&&(i=u<3?X:i,u=1),t=sl(t);++e-1?u[i?t[o]:o]:X}}function ti(n){return yi(function(t){var r=t.length,e=r,u=Y.prototype.thru;for(n&&t.reverse();e--;){var i=t[e];if("function"!=typeof i)throw new vl(en);if(u&&!o&&"wrapper"==wi(i))var o=new Y([],!0)}for(e=o?e:r;++e=tn)return o.plant(e).value();for(var u=0,i=r?t[u].apply(this,n):e;++u1&&d.reverse(),s&&af))return!1;var c=i.get(n);if(c&&i.get(t))return c==t;var l=-1,s=!0,h=r&hn?new dr:X;for(i.set(n,t),i.set(t,n);++l1?"& ":"")+t[e],t=t.join(r>2?", ":" "),n.replace(Bt,"{\n/* [wrapped with "+t+"] */\n")}function Ci(n){return wh(n)||bh(n)||!!(Ul&&n&&n[Ul])}function Ui(n,t){return t=null==t?Sn:t,!!t&&("number"==typeof n||Vt.test(n))&&n>-1&&n%1==0&&n0){if(++t>=kn)return arguments[0]}else t=0;return n.apply(X,arguments)}}function no(n,t){var r=-1,e=n.length,u=e-1;for(t=t===X?e:t;++r=this.__values__.length,t=n?X:this.__values__[this.__index__++];return{done:n,value:t}}function of(){return this}function ff(n){for(var t,r=this;r instanceof J;){var e=uo(r);e.__index__=0,e.__values__=X,t?u.__wrapped__=e:t=e;var u=e;r=r.__wrapped__}return u.__wrapped__=n,t}function af(){var n=this.__wrapped__;if(n instanceof Dt){var t=n;return this.__actions__.length&&(t=new Dt(this)),t=t.reverse(),t.__actions__.push({func:tf,args:[So],thisArg:X}),new Y(t,this.__chain__)}return this.thru(So)}function cf(){return xu(this.__wrapped__,this.__actions__)}function lf(n,t,r){var e=wh(n)?o:ne;return r&&Bi(n,t,r)&&(t=X),e(n,xi(t,3))}function sf(n,t){var r=wh(n)?f:ue;return r(n,xi(t,3))}function hf(n,t){return oe(df(n,t),1)}function pf(n,t){return oe(df(n,t),En)}function vf(n,t,r){return r=r===X?1:Oa(r),oe(df(n,t),r)}function _f(n,t){var r=wh(n)?u:ds;return r(n,xi(t,3))}function gf(n,t){var r=wh(n)?i:bs;return r(n,xi(t,3))}function yf(n,t,r,e){n=Jf(n)?n:ec(n),r=r&&!e?Oa(r):0;var u=n.length;return r<0&&(r=Hl(u+r,0)),ba(n)?r<=u&&n.indexOf(t,r)>-1:!!u&&b(n,t,r)>-1}function df(n,t){var r=wh(n)?l:Ze;return r(n,xi(t,3))}function bf(n,t,r,e){return null==n?[]:(wh(t)||(t=null==t?[]:[t]),r=e?X:r,wh(r)||(r=null==r?[]:[r]),Ye(n,t,r))}function wf(n,t,r){var e=wh(n)?h:k,u=arguments.length<3;return e(n,xi(t,4),r,u,ds)}function mf(n,t,r){var e=wh(n)?p:k,u=arguments.length<3;return e(n,xi(t,4),r,u,bs)}function xf(n,t){var r=wh(n)?f:ue;return r(n,Bf(xi(t,3)))}function jf(n){var t=wh(n)?Rr:fu;return t(n)}function Af(n,t,r){t=(r?Bi(n,t,r):t===X)?1:Oa(t);var e=wh(n)?zr:au;return e(n,t)}function kf(n){var t=wh(n)?Er:lu;return t(n)}function Of(n){if(null==n)return 0;if(Jf(n))return ba(n)?G(n):n.length;var t=Es(n);return t==Vn||t==nt?n.size:Ne(n).length}function If(n,t,r){var e=wh(n)?v:hu;return r&&Bi(n,t,r)&&(t=X),e(n,xi(t,3))}function Rf(n,t){if("function"!=typeof t)throw new vl(en);return n=Oa(n),function(){if(--n<1)return t.apply(this,arguments)}}function zf(n,t,r){return t=r?X:t,t=n&&null==t?n.length:t,pi(n,wn,X,X,X,X,t)}function Ef(n,t){var r;if("function"!=typeof t)throw new vl(en);return n=Oa(n),function(){return--n>0&&(r=t.apply(this,arguments)),n<=1&&(t=X),r}}function Sf(n,t,r){t=r?X:t;var e=pi(n,gn,X,X,X,X,X,t);return e.placeholder=Sf.placeholder,e}function Wf(n,t,r){t=r?X:t;var e=pi(n,yn,X,X,X,X,X,t);return e.placeholder=Wf.placeholder,e}function Lf(n,t,r){function e(t){var r=h,e=p;return h=p=X,d=t,_=n.apply(e,r)}function u(n){return d=n,g=Ls(f,t),b?e(n):_}function i(n){var r=n-y,e=n-d,u=t-r;return w?Jl(u,v-e):u}function o(n){var r=n-y,e=n-d;return y===X||r>=t||r<0||w&&e>=v}function f(){var n=ah();return o(n)?a(n):void(g=Ls(f,i(n)))}function a(n){return g=X,m&&h?e(n):(h=p=X,_)}function c(){g!==X&&ks(g),d=0,h=y=p=g=X}function l(){return g===X?_:a(ah())}function s(){var n=ah(),r=o(n);if(h=arguments,p=this,y=n,r){if(g===X)return u(y);if(w)return g=Ls(f,t),e(y)}return g===X&&(g=Ls(f,t)),_}var h,p,v,_,g,y,d=0,b=!1,w=!1,m=!0;if("function"!=typeof n)throw new vl(en);return t=Ra(t)||0,aa(r)&&(b=!!r.leading,w="maxWait"in r,v=w?Hl(Ra(r.maxWait)||0,t):v,m="trailing"in r?!!r.trailing:m),s.cancel=c,s.flush=l,s}function Cf(n){return pi(n,xn)}function Uf(n,t){if("function"!=typeof n||null!=t&&"function"!=typeof t)throw new vl(en);var r=function(){var e=arguments,u=t?t.apply(this,e):e[0],i=r.cache;if(i.has(u))return i.get(u);var o=n.apply(this,e);return r.cache=i.set(u,o)||i,o};return r.cache=new(Uf.Cache||hr),r}function Bf(n){if("function"!=typeof n)throw new vl(en);return function(){var t=arguments;switch(t.length){case 0:return!n.call(this);case 1:return!n.call(this,t[0]);case 2:return!n.call(this,t[0],t[1]);case 3:return!n.call(this,t[0],t[1],t[2])}return!n.apply(this,t)}}function Tf(n){return Ef(2,n)}function $f(n,t){if("function"!=typeof n)throw new vl(en);return t=t===X?t:Oa(t),ou(n,t)}function Df(n,t){if("function"!=typeof n)throw new vl(en);return t=t===X?0:Hl(Oa(t),0),ou(function(e){var u=e[t],i=Ru(e,0,t);return u&&s(i,u),r(n,this,i)})}function Mf(n,t,r){var e=!0,u=!0;if("function"!=typeof n)throw new vl(en);return aa(r)&&(e="leading"in r?!!r.leading:e,u="trailing"in r?!!r.trailing:u),Lf(n,t,{leading:e,maxWait:t,trailing:u})}function Ff(n){return zf(n,1)}function Nf(n,t){return vh(Ou(t),n)}function Pf(){if(!arguments.length)return[];var n=arguments[0];return wh(n)?n:[n]}function qf(n){return Pr(n,ln)}function Zf(n,t){return t="function"==typeof t?t:X,Pr(n,ln,t)}function Kf(n){return Pr(n,an|ln)}function Vf(n,t){return t="function"==typeof t?t:X,Pr(n,an|ln,t)}function Gf(n,t){return null==t||Hr(n,t,qa(t))}function Hf(n,t){return n===t||n!==n&&t!==t}function Jf(n){return null!=n&&fa(n.length)&&!ia(n)}function Yf(n){return ca(n)&&Jf(n)}function Qf(n){return n===!0||n===!1||ca(n)&&xe(n)==Fn}function Xf(n){return ca(n)&&1===n.nodeType&&!ya(n)}function na(n){if(null==n)return!0;if(Jf(n)&&(wh(n)||"string"==typeof n||"function"==typeof n.splice||xh(n)||Ih(n)||bh(n)))return!n.length;var t=Es(n);if(t==Vn||t==nt)return!n.size;if(Fi(n))return!Ne(n).length;for(var r in n)if(wl.call(n,r))return!1;return!0}function ta(n,t){return Le(n,t)}function ra(n,t,r){r="function"==typeof r?r:X;var e=r?r(n,t):X;return e===X?Le(n,t,X,r):!!e}function ea(n){if(!ca(n))return!1;var t=xe(n);return t==qn||t==Pn||"string"==typeof n.message&&"string"==typeof n.name&&!ya(n)}function ua(n){return"number"==typeof n&&Kl(n)}function ia(n){if(!aa(n))return!1;var t=xe(n);return t==Zn||t==Kn||t==Mn||t==Qn}function oa(n){return"number"==typeof n&&n==Oa(n)}function fa(n){return"number"==typeof n&&n>-1&&n%1==0&&n<=Sn}function aa(n){var t=typeof n;return null!=n&&("object"==t||"function"==t)}function ca(n){return null!=n&&"object"==typeof n}function la(n,t){return n===t||Be(n,t,Ai(t))}function sa(n,t,r){return r="function"==typeof r?r:X,Be(n,t,Ai(t),r)}function ha(n){return ga(n)&&n!=+n}function pa(n){if(Ss(n))throw new al(rn);return Te(n)}function va(n){return null===n}function _a(n){return null==n}function ga(n){return"number"==typeof n||ca(n)&&xe(n)==Gn}function ya(n){if(!ca(n)||xe(n)!=Jn)return!1;var t=Sl(n);if(null===t)return!0;var r=wl.call(t,"constructor")&&t.constructor;return"function"==typeof r&&r instanceof r&&bl.call(r)==Al}function da(n){return oa(n)&&n>=-Sn&&n<=Sn}function ba(n){return"string"==typeof n||!wh(n)&&ca(n)&&xe(n)==tt}function wa(n){return"symbol"==typeof n||ca(n)&&xe(n)==rt}function ma(n){return n===X}function xa(n){return ca(n)&&Es(n)==ut}function ja(n){return ca(n)&&xe(n)==it}function Aa(n){if(!n)return[];if(Jf(n))return ba(n)?H(n):Fu(n);if(Bl&&n[Bl])return M(n[Bl]());var t=Es(n),r=t==Vn?F:t==nt?q:ec;return r(n)}function ka(n){if(!n)return 0===n?n:0;if(n=Ra(n),n===En||n===-En){var t=n<0?-1:1;return t*Wn}return n===n?n:0}function Oa(n){var t=ka(n),r=t%1;return t===t?r?t-r:t:0}function Ia(n){return n?Nr(Oa(n),0,Cn):0}function Ra(n){if("number"==typeof n)return n;if(wa(n))return Ln;if(aa(n)){var t="function"==typeof n.valueOf?n.valueOf():n;n=aa(t)?t+"":t}if("string"!=typeof n)return 0===n?n:+n;n=n.replace(Lt,"");var r=qt.test(n);return r||Kt.test(n)?Xr(n.slice(2),r?2:8):Pt.test(n)?Ln:+n}function za(n){return Nu(n,Za(n))}function Ea(n){return Nr(Oa(n),-Sn,Sn)}function Sa(n){return null==n?"":yu(n)}function Wa(n,t){var r=ys(n);return null==t?r:Br(r,t)}function La(n,t){return y(n,xi(t,3),fe)}function Ca(n,t){return y(n,xi(t,3),ve)}function Ua(n,t){return null==n?n:ws(n,xi(t,3),Za)}function Ba(n,t){return null==n?n:ms(n,xi(t,3),Za)}function Ta(n,t){return n&&fe(n,xi(t,3))}function $a(n,t){return n&&ve(n,xi(t,3))}function Da(n){return null==n?[]:de(n,qa(n))}function Ma(n){return null==n?[]:de(n,Za(n))}function Fa(n,t,r){var e=null==n?X:we(n,t);return e===X?r:e}function Na(n,t){return null!=n&&zi(n,t,Ae)}function Pa(n,t){return null!=n&&zi(n,t,ke)}function qa(n){return Jf(n)?Ir(n):Ne(n)}function Za(n){return Jf(n)?Ir(n,!0):Pe(n)}function Ka(n,t){var r={};return t=xi(t,3),fe(n,function(n,e,u){Mr(r,t(n,e,u),n)}),r}function Va(n,t){var r={};return t=xi(t,3),fe(n,function(n,e,u){Mr(r,e,t(n,e,u))}),r}function Ga(n,t){return Ha(n,Bf(xi(t)))}function Ha(n,t){if(null==n)return{};var r=l(bi(n),function(n){return[n]});return t=xi(t),Xe(n,r,function(n,r){return t(n,r[0])})}function Ja(n,t,r){t=Iu(t,n);var e=-1,u=t.length;for(u||(u=1,n=X);++et){var e=n;n=t,t=e}if(r||n%1||t%1){var u=Xl();return Jl(n+u*(t-n+Qr("1e-"+((u+"").length-1))),t)}return eu(n,t)}function ac(n){return Xh(Sa(n).toLowerCase())}function cc(n){return n=Sa(n),n&&n.replace(Gt,_e).replace(Dr,"")}function lc(n,t,r){n=Sa(n),t=yu(t);var e=n.length;r=r===X?e:Nr(Oa(r),0,e);var u=r;return r-=t.length,r>=0&&n.slice(r,u)==t}function sc(n){return n=Sa(n),n&&jt.test(n)?n.replace(mt,ge):n}function hc(n){return n=Sa(n),n&&Wt.test(n)?n.replace(St,"\\$&"):n}function pc(n,t,r){n=Sa(n),t=Oa(t);var e=t?G(n):0;if(!t||e>=t)return n;var u=(t-e)/2;return oi(Pl(u),r)+n+oi(Nl(u),r)}function vc(n,t,r){n=Sa(n),t=Oa(t);var e=t?G(n):0;return t&&e>>0)?(n=Sa(n),n&&("string"==typeof t||null!=t&&!kh(t))&&(t=yu(t),!t&&$(n))?Ru(H(n),0,r):n.split(t,r)):[]}function wc(n,t,r){return n=Sa(n),r=Nr(Oa(r),0,n.length),t=yu(t),n.slice(r,r+t.length)==t}function mc(n,t,r){var e=K.templateSettings;r&&Bi(n,t,r)&&(t=X),n=Sa(n),t=Wh({},t,e,Sr);var u,i,o=Wh({},t.imports,e.imports,Sr),f=qa(o),a=S(o,f),c=0,l=t.interpolate||Ht,s="__p += '",h=hl((t.escape||Ht).source+"|"+l.source+"|"+(l===Ot?Ft:Ht).source+"|"+(t.evaluate||Ht).source+"|$","g"),p="//# sourceURL="+("sourceURL"in t?t.sourceURL:"lodash.templateSources["+ ++Zr+"]")+"\n";n.replace(h,function(t,r,e,o,f,a){return e||(e=o),s+=n.slice(c,a).replace(Jt,B),r&&(u=!0,s+="' +\n__e("+r+") +\n'"),f&&(i=!0,s+="';\n"+f+";\n__p += '"),e&&(s+="' +\n((__t = ("+e+")) == null ? '' : __t) +\n'"),c=a+t.length,t}),s+="';\n";var v=t.variable;v||(s="with (obj) {\n"+s+"\n}\n"),s=(i?s.replace(yt,""):s).replace(dt,"$1").replace(bt,"$1;"),s="function("+(v||"obj")+") {\n"+(v?"":"obj || (obj = {});\n")+"var __t, __p = ''"+(u?", __e = _.escape":"")+(i?", __j = Array.prototype.join;\nfunction print() { __p += __j.call(arguments, '') }\n":";\n")+s+"return __p\n}";var _=np(function(){return cl(f,p+"return "+s).apply(X,a)});if(_.source=s,ea(_))throw _;return _}function xc(n){return Sa(n).toLowerCase()}function jc(n){return Sa(n).toUpperCase()}function Ac(n,t,r){if(n=Sa(n),n&&(r||t===X))return n.replace(Lt,"");if(!n||!(t=yu(t)))return n;var e=H(n),u=H(t),i=L(e,u),o=C(e,u)+1;return Ru(e,i,o).join("")}function kc(n,t,r){if(n=Sa(n),n&&(r||t===X))return n.replace(Ut,"");if(!n||!(t=yu(t)))return n;var e=H(n),u=C(e,H(t))+1;return Ru(e,0,u).join("")}function Oc(n,t,r){if(n=Sa(n),n&&(r||t===X))return n.replace(Ct,"");if(!n||!(t=yu(t)))return n;var e=H(n),u=L(e,H(t));return Ru(e,u).join("")}function Ic(n,t){var r=jn,e=An;if(aa(t)){var u="separator"in t?t.separator:u;r="length"in t?Oa(t.length):r,e="omission"in t?yu(t.omission):e}n=Sa(n);var i=n.length;if($(n)){var o=H(n);i=o.length}if(r>=i)return n;var f=r-G(e);if(f<1)return e;var a=o?Ru(o,0,f).join(""):n.slice(0,f);if(u===X)return a+e;if(o&&(f+=a.length-f),kh(u)){if(n.slice(f).search(u)){var c,l=a;for(u.global||(u=hl(u.source,Sa(Nt.exec(u))+"g")),u.lastIndex=0;c=u.exec(l);)var s=c.index;a=a.slice(0,s===X?f:s)}}else if(n.indexOf(yu(u),f)!=f){var h=a.lastIndexOf(u);h>-1&&(a=a.slice(0,h))}return a+e}function Rc(n){return n=Sa(n),n&&xt.test(n)?n.replace(wt,ye):n}function zc(n,t,r){return n=Sa(n),t=r?X:t,t===X?D(n)?Q(n):g(n):n.match(t)||[]}function Ec(n){var t=null==n?0:n.length,e=xi();return n=t?l(n,function(n){if("function"!=typeof n[1])throw new vl(en);return[e(n[0]),n[1]]}):[],ou(function(e){for(var u=-1;++uSn)return[];var r=Cn,e=Jl(n,Cn);t=xi(t),n-=Cn;for(var u=R(e,t);++r1?n[t-1]:X;return r="function"==typeof r?(n.pop(),r):X,Jo(n,r)}),Xs=yi(function(n){var t=n.length,r=t?n[0]:0,e=this.__wrapped__,u=function(t){return Fr(t,n)};return!(t>1||this.__actions__.length)&&e instanceof Dt&&Ui(r)?(e=e.slice(r,+r+(t?1:0)),e.__actions__.push({func:tf,args:[u],thisArg:X}),new Y(e,this.__chain__).thru(function(n){return t&&!n.length&&n.push(X),n})):this.thru(u)}),nh=Zu(function(n,t,r){wl.call(n,r)?++n[r]:Mr(n,r,1)}),th=ni(po),rh=ni(vo),eh=Zu(function(n,t,r){wl.call(n,r)?n[r].push(t):Mr(n,r,[t])}),uh=ou(function(n,t,e){var u=-1,i="function"==typeof t,o=Jf(n)?ol(n.length):[];return ds(n,function(n){o[++u]=i?r(t,n,e):ze(n,t,e)}),o}),ih=Zu(function(n,t,r){Mr(n,r,t)}),oh=Zu(function(n,t,r){n[r?0:1].push(t)},function(){return[[],[]]}),fh=ou(function(n,t){if(null==n)return[];var r=t.length;return r>1&&Bi(n,t[0],t[1])?t=[]:r>2&&Bi(t[0],t[1],t[2])&&(t=[t[0]]),Ye(n,oe(t,1),[])}),ah=Ml||function(){return re.Date.now()},ch=ou(function(n,t,r){var e=pn;if(r.length){var u=P(r,mi(ch));e|=dn}return pi(n,e,t,r,u)}),lh=ou(function(n,t,r){var e=pn|vn;if(r.length){var u=P(r,mi(lh));e|=dn}return pi(t,e,n,r,u)}),sh=ou(function(n,t){return Jr(n,1,t)}),hh=ou(function(n,t,r){return Jr(n,Ra(t)||0,r)});Uf.Cache=hr;var ph=As(function(n,t){t=1==t.length&&wh(t[0])?l(t[0],E(xi())):l(oe(t,1),E(xi()));var e=t.length;return ou(function(u){for(var i=-1,o=Jl(u.length,e);++i=t}),bh=Ee(function(){return arguments}())?Ee:function(n){return ca(n)&&wl.call(n,"callee")&&!Ll.call(n,"callee")},wh=ol.isArray,mh=ae?E(ae):Se,xh=Zl||Zc,jh=ce?E(ce):We,Ah=le?E(le):Ue,kh=se?E(se):$e,Oh=he?E(he):De,Ih=pe?E(pe):Me,Rh=ci(qe),zh=ci(function(n,t){return n<=t}),Eh=Ku(function(n,t){if(Fi(t)||Jf(t))return void Nu(t,qa(t),n);for(var r in t)wl.call(t,r)&&Lr(n,r,t[r])}),Sh=Ku(function(n,t){Nu(t,Za(t),n)}),Wh=Ku(function(n,t,r,e){Nu(t,Za(t),n,e)}),Lh=Ku(function(n,t,r,e){Nu(t,qa(t),n,e)}),Ch=yi(Fr),Uh=ou(function(n){return n.push(X,Sr),r(Wh,X,n)}),Bh=ou(function(n){return n.push(X,Ki),r(Fh,X,n)}),Th=ei(function(n,t,r){n[t]=r},Wc(Cc)),$h=ei(function(n,t,r){wl.call(n,t)?n[t].push(r):n[t]=[r]},xi),Dh=ou(ze),Mh=Ku(function(n,t,r){Ge(n,t,r)}),Fh=Ku(function(n,t,r,e){Ge(n,t,r,e)}),Nh=yi(function(n,t){var r={};if(null==n)return r;var e=!1;t=l(t,function(t){return t=Iu(t,n),e||(e=t.length>1),t}),Nu(n,bi(n),r),e&&(r=Pr(r,an|cn|ln));for(var u=t.length;u--;)bu(r,t[u]);return r}),Ph=yi(function(n,t){return null==n?{}:Qe(n,t)}),qh=hi(qa),Zh=hi(Za),Kh=Yu(function(n,t,r){return t=t.toLowerCase(),n+(r?ac(t):t)}),Vh=Yu(function(n,t,r){return n+(r?"-":"")+t.toLowerCase()}),Gh=Yu(function(n,t,r){return n+(r?" ":"")+t.toLowerCase()}),Hh=Ju("toLowerCase"),Jh=Yu(function(n,t,r){return n+(r?"_":"")+t.toLowerCase()}),Yh=Yu(function(n,t,r){return n+(r?" ":"")+Xh(t)}),Qh=Yu(function(n,t,r){return n+(r?" ":"")+t.toUpperCase()}),Xh=Ju("toUpperCase"),np=ou(function(n,t){try{return r(n,X,t)}catch(n){return ea(n)?n:new al(n)}}),tp=yi(function(n,t){return u(t,function(t){t=to(t),Mr(n,t,ch(n[t],n))}),n}),rp=ti(),ep=ti(!0),up=ou(function(n,t){return function(r){return ze(r,n,t)}}),ip=ou(function(n,t){return function(r){return ze(n,r,t)}}),op=ii(l),fp=ii(o),ap=ii(v),cp=ai(),lp=ai(!0),sp=ui(function(n,t){return n+t},0),hp=si("ceil"),pp=ui(function(n,t){return n/t},1),vp=si("floor"),_p=ui(function(n,t){return n*t},1),gp=si("round"),yp=ui(function(n,t){return n-t},0);return K.after=Rf,K.ary=zf,K.assign=Eh,K.assignIn=Sh,K.assignInWith=Wh,K.assignWith=Lh,K.at=Ch,K.before=Ef,K.bind=ch,K.bindAll=tp,K.bindKey=lh,K.castArray=Pf,K.chain=Xo,K.chunk=io,K.compact=oo,K.concat=fo,K.cond=Ec,K.conforms=Sc,K.constant=Wc,K.countBy=nh,K.create=Wa,K.curry=Sf,K.curryRight=Wf,K.debounce=Lf,K.defaults=Uh,K.defaultsDeep=Bh,K.defer=sh,K.delay=hh,K.difference=Bs,K.differenceBy=Ts,K.differenceWith=$s,K.drop=ao,K.dropRight=co, +K.dropRightWhile=lo,K.dropWhile=so,K.fill=ho,K.filter=sf,K.flatMap=hf,K.flatMapDeep=pf,K.flatMapDepth=vf,K.flatten=_o,K.flattenDeep=go,K.flattenDepth=yo,K.flip=Cf,K.flow=rp,K.flowRight=ep,K.fromPairs=bo,K.functions=Da,K.functionsIn=Ma,K.groupBy=eh,K.initial=xo,K.intersection=Ds,K.intersectionBy=Ms,K.intersectionWith=Fs,K.invert=Th,K.invertBy=$h,K.invokeMap=uh,K.iteratee=Uc,K.keyBy=ih,K.keys=qa,K.keysIn=Za,K.map=df,K.mapKeys=Ka,K.mapValues=Va,K.matches=Bc,K.matchesProperty=Tc,K.memoize=Uf,K.merge=Mh,K.mergeWith=Fh,K.method=up,K.methodOf=ip,K.mixin=$c,K.negate=Bf,K.nthArg=Fc,K.omit=Nh,K.omitBy=Ga,K.once=Tf,K.orderBy=bf,K.over=op,K.overArgs=ph,K.overEvery=fp,K.overSome=ap,K.partial=vh,K.partialRight=_h,K.partition=oh,K.pick=Ph,K.pickBy=Ha,K.property=Nc,K.propertyOf=Pc,K.pull=Ns,K.pullAll=Io,K.pullAllBy=Ro,K.pullAllWith=zo,K.pullAt=Ps,K.range=cp,K.rangeRight=lp,K.rearg=gh,K.reject=xf,K.remove=Eo,K.rest=$f,K.reverse=So,K.sampleSize=Af,K.set=Ya,K.setWith=Qa,K.shuffle=kf,K.slice=Wo,K.sortBy=fh,K.sortedUniq=Do,K.sortedUniqBy=Mo,K.split=bc,K.spread=Df,K.tail=Fo,K.take=No,K.takeRight=Po,K.takeRightWhile=qo,K.takeWhile=Zo,K.tap=nf,K.throttle=Mf,K.thru=tf,K.toArray=Aa,K.toPairs=qh,K.toPairsIn=Zh,K.toPath=Jc,K.toPlainObject=za,K.transform=Xa,K.unary=Ff,K.union=qs,K.unionBy=Zs,K.unionWith=Ks,K.uniq=Ko,K.uniqBy=Vo,K.uniqWith=Go,K.unset=nc,K.unzip=Ho,K.unzipWith=Jo,K.update=tc,K.updateWith=rc,K.values=ec,K.valuesIn=uc,K.without=Vs,K.words=zc,K.wrap=Nf,K.xor=Gs,K.xorBy=Hs,K.xorWith=Js,K.zip=Ys,K.zipObject=Yo,K.zipObjectDeep=Qo,K.zipWith=Qs,K.entries=qh,K.entriesIn=Zh,K.extend=Sh,K.extendWith=Wh,$c(K,K),K.add=sp,K.attempt=np,K.camelCase=Kh,K.capitalize=ac,K.ceil=hp,K.clamp=ic,K.clone=qf,K.cloneDeep=Kf,K.cloneDeepWith=Vf,K.cloneWith=Zf,K.conformsTo=Gf,K.deburr=cc,K.defaultTo=Lc,K.divide=pp,K.endsWith=lc,K.eq=Hf,K.escape=sc,K.escapeRegExp=hc,K.every=lf,K.find=th,K.findIndex=po,K.findKey=La,K.findLast=rh,K.findLastIndex=vo,K.findLastKey=Ca,K.floor=vp,K.forEach=_f,K.forEachRight=gf,K.forIn=Ua,K.forInRight=Ba,K.forOwn=Ta,K.forOwnRight=$a,K.get=Fa,K.gt=yh,K.gte=dh,K.has=Na,K.hasIn=Pa,K.head=wo,K.identity=Cc,K.includes=yf,K.indexOf=mo,K.inRange=oc,K.invoke=Dh,K.isArguments=bh,K.isArray=wh,K.isArrayBuffer=mh,K.isArrayLike=Jf,K.isArrayLikeObject=Yf,K.isBoolean=Qf,K.isBuffer=xh,K.isDate=jh,K.isElement=Xf,K.isEmpty=na,K.isEqual=ta,K.isEqualWith=ra,K.isError=ea,K.isFinite=ua,K.isFunction=ia,K.isInteger=oa,K.isLength=fa,K.isMap=Ah,K.isMatch=la,K.isMatchWith=sa,K.isNaN=ha,K.isNative=pa,K.isNil=_a,K.isNull=va,K.isNumber=ga,K.isObject=aa,K.isObjectLike=ca,K.isPlainObject=ya,K.isRegExp=kh,K.isSafeInteger=da,K.isSet=Oh,K.isString=ba,K.isSymbol=wa,K.isTypedArray=Ih,K.isUndefined=ma,K.isWeakMap=xa,K.isWeakSet=ja,K.join=jo,K.kebabCase=Vh,K.last=Ao,K.lastIndexOf=ko,K.lowerCase=Gh,K.lowerFirst=Hh,K.lt=Rh,K.lte=zh,K.max=Qc,K.maxBy=Xc,K.mean=nl,K.meanBy=tl,K.min=rl,K.minBy=el,K.stubArray=qc,K.stubFalse=Zc,K.stubObject=Kc,K.stubString=Vc,K.stubTrue=Gc,K.multiply=_p,K.nth=Oo,K.noConflict=Dc,K.noop=Mc,K.now=ah,K.pad=pc,K.padEnd=vc,K.padStart=_c,K.parseInt=gc,K.random=fc,K.reduce=wf,K.reduceRight=mf,K.repeat=yc,K.replace=dc,K.result=Ja,K.round=gp,K.runInContext=_,K.sample=jf,K.size=Of,K.snakeCase=Jh,K.some=If,K.sortedIndex=Lo,K.sortedIndexBy=Co,K.sortedIndexOf=Uo,K.sortedLastIndex=Bo,K.sortedLastIndexBy=To,K.sortedLastIndexOf=$o,K.startCase=Yh,K.startsWith=wc,K.subtract=yp,K.sum=ul,K.sumBy=il,K.template=mc,K.times=Hc,K.toFinite=ka,K.toInteger=Oa,K.toLength=Ia,K.toLower=xc,K.toNumber=Ra,K.toSafeInteger=Ea,K.toString=Sa,K.toUpper=jc,K.trim=Ac,K.trimEnd=kc,K.trimStart=Oc,K.truncate=Ic,K.unescape=Rc,K.uniqueId=Yc,K.upperCase=Qh,K.upperFirst=Xh,K.each=_f,K.eachRight=gf,K.first=wo,$c(K,function(){var n={};return fe(K,function(t,r){wl.call(K.prototype,r)||(n[r]=t)}),n}(),{chain:!1}),K.VERSION=nn,u(["bind","bindKey","curry","curryRight","partial","partialRight"],function(n){K[n].placeholder=K}),u(["drop","take"],function(n,t){Dt.prototype[n]=function(r){var e=this.__filtered__;if(e&&!t)return new Dt(this);r=r===X?1:Hl(Oa(r),0);var u=this.clone();return e?u.__takeCount__=Jl(r,u.__takeCount__):u.__views__.push({size:Jl(r,Cn),type:n+(u.__dir__<0?"Right":"")}),u},Dt.prototype[n+"Right"]=function(t){return this.reverse()[n](t).reverse()}}),u(["filter","map","takeWhile"],function(n,t){var r=t+1,e=r==In||r==zn;Dt.prototype[n]=function(n){var t=this.clone();return t.__iteratees__.push({iteratee:xi(n,3),type:r}),t.__filtered__=t.__filtered__||e,t}}),u(["head","last"],function(n,t){var r="take"+(t?"Right":"");Dt.prototype[n]=function(){return this[r](1).value()[0]}}),u(["initial","tail"],function(n,t){var r="drop"+(t?"":"Right");Dt.prototype[n]=function(){return this.__filtered__?new Dt(this):this[r](1)}}),Dt.prototype.compact=function(){return this.filter(Cc)},Dt.prototype.find=function(n){return this.filter(n).head()},Dt.prototype.findLast=function(n){return this.reverse().find(n)},Dt.prototype.invokeMap=ou(function(n,t){return"function"==typeof n?new Dt(this):this.map(function(r){return ze(r,n,t)})}),Dt.prototype.reject=function(n){return this.filter(Bf(xi(n)))},Dt.prototype.slice=function(n,t){n=Oa(n);var r=this;return r.__filtered__&&(n>0||t<0)?new Dt(r):(n<0?r=r.takeRight(-n):n&&(r=r.drop(n)),t!==X&&(t=Oa(t),r=t<0?r.dropRight(-t):r.take(t-n)),r)},Dt.prototype.takeRightWhile=function(n){return this.reverse().takeWhile(n).reverse()},Dt.prototype.toArray=function(){return this.take(Cn)},fe(Dt.prototype,function(n,t){var r=/^(?:filter|find|map|reject)|While$/.test(t),e=/^(?:head|last)$/.test(t),u=K[e?"take"+("last"==t?"Right":""):t],i=e||/^find/.test(t);u&&(K.prototype[t]=function(){var t=this.__wrapped__,o=e?[1]:arguments,f=t instanceof Dt,a=o[0],c=f||wh(t),l=function(n){var t=u.apply(K,s([n],o));return e&&h?t[0]:t};c&&r&&"function"==typeof a&&1!=a.length&&(f=c=!1);var h=this.__chain__,p=!!this.__actions__.length,v=i&&!h,_=f&&!p;if(!i&&c){t=_?t:new Dt(this);var g=n.apply(t,o);return g.__actions__.push({func:tf,args:[l],thisArg:X}),new Y(g,h)}return v&&_?n.apply(this,o):(g=this.thru(l),v?e?g.value()[0]:g.value():g)})}),u(["pop","push","shift","sort","splice","unshift"],function(n){var t=_l[n],r=/^(?:push|sort|unshift)$/.test(n)?"tap":"thru",e=/^(?:pop|shift)$/.test(n);K.prototype[n]=function(){var n=arguments;if(e&&!this.__chain__){var u=this.value();return t.apply(wh(u)?u:[],n)}return this[r](function(r){return t.apply(wh(r)?r:[],n)})}}),fe(Dt.prototype,function(n,t){var r=K[t];if(r){var e=r.name+"",u=as[e]||(as[e]=[]);u.push({name:t,func:r})}}),as[ri(X,vn).name]=[{name:"wrapper",func:X}],Dt.prototype.clone=Yt,Dt.prototype.reverse=Qt,Dt.prototype.value=Xt,K.prototype.at=Xs,K.prototype.chain=rf,K.prototype.commit=ef,K.prototype.next=uf,K.prototype.plant=ff,K.prototype.reverse=af,K.prototype.toJSON=K.prototype.valueOf=K.prototype.value=cf,K.prototype.first=K.prototype.head,Bl&&(K.prototype[Bl]=of),K},be=de();"function"==typeof define&&"object"==typeof define.amd&&define.amd?(re._=be,define(function(){return be})):ue?((ue.exports=be)._=be,ee._=be):re._=be}).call(this); diff --git a/moz/manifest.json b/moz/manifest.json new file mode 100644 index 0000000..105a05f --- /dev/null +++ b/moz/manifest.json @@ -0,0 +1,230 @@ +{ +// "update_url": "https://ephellon.github.com/web.to.plex/update.xml", + + "name": "Web to Plex", + "description": "Adds a button on various movie & TV show sites to open the item in Plex, or send to your designated NZB manager for download.", + "homepage_url": "https://github.com/SpaceK33z/web-to-plex/", + + "manifest_version": 2, + "version": "4.1.0.3", +// Firefox Support => + "applications": { + "gecko": { + "id": "minkcbos@gmail.com", + //"{05243336-ce19-46df-95af-680070c96134}" + "strict_min_version": "57.0" + } + }, + + "icons": { + "16": "16.png", + "32": "32.png", + "48": "48.png", + "96": "96.png", + "128": "128.png", + "256": "256.png" + }, + + "content_scripts": [ + // Allows media downloads + { + "matches": [ + "*://*.openload.co/*", "*://*.oload.co/*", + "*://*.openload.com/*", "*://*.oload.com/*", + "*://*.openload.fun/*", "*://*.oload.fun/*", + "*://*.openload.biz/*", "*://*.oload.biz/*", + "*://*.openload.vip/*", "*://*.oload.vip/*", + "*://*.openload.club/*", "*://*.oload.club/*", + "*://*.openload.io/*", "*://*.oload.io/*", + "*://*.openload.xyz/*", "*://*.oload.xyz/*", + "*://*.openload.cc/*", "*://*.oload.cc/*", + "*://*.openload.to/*", "*://*.oload.to/*", + "*://*.openload.is/*", "*://*.oload.is/*", + "*://*.openload.gg/*", "*://*.oload.gg/*", + "*://*.openload.tv/*", "*://*.oload.tv/*", + "*://*.openload.fm/*", "*://*.oload.fm/*", + "*://*.openload.cx/*", "*://*.oload.cx/*", + "*://*.openload.ac/*", "*://*.oload.ac/*", + "*://*.openload.name/*", "*://*.oload.name/*", + "*://*.openload.global/*", "*://*.oload.global/*" + ], + "js": ["oload.js"], + "all_frames": true + }, + { + "matches": ["*://*.consistent.stream/titles/*", "*://*.consistent.stream/watch/*"], + "js": ["consistent.js"], + "all_frames": true + }, + { + "matches": ["*://app.plex.tv/desktop#!/server/*/details?*"], + "js": ["plxdwnld.js"], + "all_frames": true + }, + + // Testing purposes only + { + "matches": ["*://ephellon.github.io/web.to.plex/test/*"], + "js": ["utils.js", "__test__.js"], + "css": ["common.css", "theme.css"] + }, + + // The sites + { + "matches": ["*://*.movieo.me/*"], + "js": ["history-hack.js", "utils.js", "movieo$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://*.imdb.com/*"], + "js": ["utils.js", "imdb$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://*.trakt.tv/*"], + "js": ["history-hack.js", "utils.js", "trakt$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://*.letterboxd.com/*"], + "js": ["utils.js", "letterboxd$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://*.tvmaze.com/shows/*"], + "js": ["utils.js", "tvmaze$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://*.thetvdb.com/series/*"], + "js": ["utils.js", "tvdb$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://*.themoviedb.org/movie/*", "*://*.themoviedb.org/tv/*"], + "js": ["utils.js", "tmdb$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://*.vrv.co/*"], + "js": ["utils.js", "vrv$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://*.hulu.com/*"], + "js": ["utils.js", "hulu$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://play.google.com/store/*"], + "js": ["utils.js", "google.play$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://itunes.apple.com/*"], + "js": ["utils.js", "itunes$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://*.metacritic.com/*"], + "js": ["utils.js", "metacritic$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://*.fandango.com/*"], + "js": ["utils.js", "fandango$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://*.amazon.com/*"], + "js": ["utils.js", "amazon$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://*.vudu.com/*"], + "js": ["utils.js", "vudu$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://*.verizon.com/*"], + "js": ["utils.js", "verizon$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://*.couchpotato.life/*/*"], + "js": ["utils.js", "couchpotato$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://*.rottentomatoes.com/*/*"], + "js": ["utils.js", "rottentomatoes$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://*.netflix.com/watch/*"], + "js": ["utils.js", "netflix$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://*.vumoo.to/*"], + "js": ["utils.js", "vumoo$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://www.google.com/*"], + "js": ["utils.js", "google$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://www.youtube.com/*"], + "js": ["utils.js", "youtube$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://*.flickmetrix.com/*"], + "js": ["utils.js", "flickmetrix$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://*.justwatch.com/*"], + "js": ["utils.js", "justwatch$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://*.moviemeter.nl/*"], + "js": ["utils.js", "moviemeter$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://*.allocine.fr/*"], + "js": ["utils.js", "allocine$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://*.gostream.site/*"], + "js": ["utils.js", "gostream$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://*.tubitv.com/*"], + "js": ["utils.js", "tubi$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://ephellon.github.io/web.to.plex/?*", "*://ephellon.github.io/web.to.plex/index.html?*", "*://ephellon.github.io/web.to.plex/login.html?*"], + "js": ["utils.js", "webtoplex$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["*://app.plex.tv/desktop/*"], + "js": ["utils.js", "plex$.js"], + "css": ["common.css", "theme.css"] + },{ + "matches": ["https://*/*"], + "js": ["utils.js", "common.js"] + } + ], + + "background": { + "scripts": ["background.js", "plugn.js"], + "persistent": true + }, + + // Disable/Comment "options_page" to enable FF support + //"options_page": "options.html", + "options_ui": { + "page": "options.html", + "open_in_tab": true + }, + + "browser_action": { + "default_icon": { + "16": "16.png", + "32": "32.png", + "48": "48.png", + "96": "96.png" + }, + "default_title": "Web to Plex", + "default_popup": "popup.html" + }, + + "permissions": [ + "tabs", + "storage", + "downloads", + "contextMenus", + "" + ], + "web_accessible_resources": ["*.png", "options.*"] +} diff --git a/moz/metacritic$.js b/moz/metacritic$.js new file mode 100644 index 0000000..be3e7c5 --- /dev/null +++ b/moz/metacritic$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'metacritic' }))(); diff --git a/moz/metacritic.js b/moz/metacritic.js new file mode 100644 index 0000000..d61d41d --- /dev/null +++ b/moz/metacritic.js @@ -0,0 +1,49 @@ +let script = { + "url": "*://*.metacritic.com/(movie|tv|list)/*", + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title, year, image, + type = script.getType(); + + switch(type) { + case 'tv': + case 'movie': + title = $('.product_page_title > *, .product_title').first; + year = $('.product_page_title > .release_year, .product_data .release_data').first; + image = $('.summary_img').first; + + title = title.textContent.replace(/\s+/g, ' ').trim(); + year = +year.textContent.replace(/\s+/g, ' ').replace(/.*(\d{4}).*$/, '$1').trim(); + image = (image || {}).src; + + type = type == 'tv'? 'show': type; + + return { type, title, year, image }; + break; + + case 'list': + /* Not yet implemented */ + break; + + default: + /* Error */ + return {}; + break; + } + }, + + "getType": () => { + /^\/(movie|tv|list)\//.test(top.location.pathname); + + let type = RegExp.$1; + + return type; + }, + + "process": (element) => { + /* Not implemented... Metacritic has too much sh*t loading to even try to open a console */ + /* Targeted for v5/v6 */ + }, +}; diff --git a/moz/metacritic.png b/moz/metacritic.png new file mode 100644 index 0000000..385ca72 Binary files /dev/null and b/moz/metacritic.png differ diff --git a/moz/movie.poster.jpg b/moz/movie.poster.jpg new file mode 100644 index 0000000..efdc64f Binary files /dev/null and b/moz/movie.poster.jpg differ diff --git a/moz/moviemeter$.js b/moz/moviemeter$.js new file mode 100644 index 0000000..4bf6acf --- /dev/null +++ b/moz/moviemeter$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'moviemeter' }))(); diff --git a/moz/moviemeter.js b/moz/moviemeter.js new file mode 100644 index 0000000..84d0080 --- /dev/null +++ b/moz/moviemeter.js @@ -0,0 +1,34 @@ +let script = { + "url": "*://*.moviemeter.nl/film/\\d+", + + "ready": () => !$('.rating + p font').empty, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('.details span').first, + year = $('.details *').first, + image = $('.poster').first, + type = script.getType(); + + if(!title || !year) + return 1000; + + year = year.lastChild.textContent; + title = title.textContent.replace(year, '').trim(); + year = +year.replace(/\D+/g, ''); + image = image.src; + + return { type, title, year, image }; + }, + + "getType": () => { + let time = $('.rating + p font').last; + + time = time.textContent; + + if(/(series|show)/.test(time)) + return 'show'; + return 'film'; + }, +}; diff --git a/moz/moviemeter.png b/moz/moviemeter.png new file mode 100644 index 0000000..b153f79 Binary files /dev/null and b/moz/moviemeter.png differ diff --git a/moz/movieo$.js b/moz/movieo$.js new file mode 100644 index 0000000..7738986 --- /dev/null +++ b/moz/movieo$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'movieo' }))(); diff --git a/moz/movieo.js b/moz/movieo.js new file mode 100644 index 0000000..dbf9601 --- /dev/null +++ b/moz/movieo.js @@ -0,0 +1,75 @@ +let script = { + "url": "*://*.movieo.me/*", + + "ready": () => !$('.share-box, .zopim').empty, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title, year, image, IMDbID, + type = script.getType(); + + switch(type) { + case 'movie': + title = $('#doc_title').first; + year = $('meta[itemprop="datePublished"i]').first; + image = $('img.poster').first; + + title = title.dataset.title.trim(); + year = +year.content.slice(0, 4); + image = (image || {}).src; + IMDbID = script.getIMDbID(); + break; + + case 'list': + let items = $('[data-title][data-id]'), + options = []; + + items.forEach((element, index, array) => { + let option = script.process(element); + + if(option) + options.push(option); + }); + + return options; + break; + + default: + /* Error */ + return {}; + break; + } + + return { type, title, year, image }; + }, + + "getType": () => { + let type = /\/(black|seen|watch)?lists?\//i.test(top.location.pathname)? + 'list': + 'movie'; + + return type; + }, + + "getIMDbID": () => { + let link = $( + '.tt-parent[href*="imdb.com/title/tt"i]' + ).first; + + if(link) + return link.href.replace(/^[^]*\/title\//i, ''); + }, + + "process": (element) => { + let title = $('.title', element).first, + image = $('.poster-cont', element).first, + year, type = 'movie'; + + title = title.textContent.trim().replace(/\s*\((\d{4})\)/, ''); + year = +RegExp.$1; + image = image.getAttribute('data-src'); + + return { type, title, year, image }; + }, +}; diff --git a/moz/movieo.png b/moz/movieo.png new file mode 100644 index 0000000..05b13c7 Binary files /dev/null and b/moz/movieo.png differ diff --git a/moz/netflix$.js b/moz/netflix$.js new file mode 100644 index 0000000..5bc66c2 --- /dev/null +++ b/moz/netflix$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'netflix' }))(); diff --git a/moz/netflix.js b/moz/netflix.js new file mode 100644 index 0000000..1a22407 --- /dev/null +++ b/moz/netflix.js @@ -0,0 +1,28 @@ +let script = { + "url": "*://*.netflix.com/watch/\\d+", + + "ready": () => { + let element = $('[class$="__time"]').first; + + return element && !/^([0:]+|null|undefined)?$/.test(element.textContent); + }, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('.video-title h4').first, + year = 0, + image = '', + type = script.getType(); + + title = title.textContent; + + return { type, title, year, image }; + }, + + "getType": () => { + let element = $('[class*="playerEpisodes"]').first; + + return !!element? 'show': 'movie'; + }, +}; diff --git a/moz/netflix.png b/moz/netflix.png new file mode 100644 index 0000000..e6087a0 Binary files /dev/null and b/moz/netflix.png differ diff --git a/moz/noise.png b/moz/noise.png new file mode 100644 index 0000000..7eead13 Binary files /dev/null and b/moz/noise.png differ diff --git a/moz/null.png b/moz/null.png new file mode 100644 index 0000000..be90c6b Binary files /dev/null and b/moz/null.png differ diff --git a/moz/o16.png b/moz/o16.png new file mode 100644 index 0000000..2256345 Binary files /dev/null and b/moz/o16.png differ diff --git a/moz/o48.png b/moz/o48.png new file mode 100644 index 0000000..010c98c Binary files /dev/null and b/moz/o48.png differ diff --git a/moz/oload.js b/moz/oload.js new file mode 100644 index 0000000..732f588 --- /dev/null +++ b/moz/oload.js @@ -0,0 +1,29 @@ +let NO_DEBUGGER = false; + +let terminal = + NO_DEBUGGER? + { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m }: + console; + +let check; + +check = document.body.onload = event => { + let video = document.querySelector('div > p + p'); + + if(video && (video.src || video.textContent)) { + let { src } = video; + + src = src || video.textContent; + + if(/^blob:/i.test(src)) + throw ' URL detected. Unable to reform file.'; + + try { + top.postMessage({ href: `https://oload.fun/stream/${ src }?mime=true`, tail: 'MP4', type: 'SEND_VIDEO_LINK', from: 'oload' }, '*'); + } catch(error) { + terminal.error('Failed to post message:', error); + } + } else { + setTimeout(check, 500); + } +}; diff --git a/moz/options.css b/moz/options.css new file mode 100644 index 0000000..60e88d5 --- /dev/null +++ b/moz/options.css @@ -0,0 +1,647 @@ +html, body { + height: 100%; +} + +body { + background: url(noise.png) fixed, url(256.png) no-repeat fixed center, #3f4245 !important; + color: #333 !important; + font-family: Open Sans Regular, Helvetica Neue, Helvetica, Arial, sans-serif, system; + font-size: 18px !important; + padding: 1em; + width: 90vw; + flex-grow: 1; + padding: 25px; + overflow: auto; + position: absolute; +} + +a { + color: #cc7b19 !important; + text-decoration: none !important; +} + +a[target="_blank"]::after { + content: " [\2197]"; + font-size: 70%; + + vertical-align: super; +} + +h1 { + position: sticky; + top: 0; +} + +h2 { + text-align: center; +} + +hr { + border: 1px solid #6668; +} +hr:last-child { + display: none; +} + +article { + z-index: 3; +} + +section { + display: block !important; + margin-bottom: 20px; + box-sizing: border-box; +} + +label { + color: #eee !important; + font-weight: 400 !important; + display: inline-block; + margin-bottom: 5px; +} + +input[type="text"], input[type="password"], textarea, select { + width: 30vw !important; + line-height: 1.5em !important; + transition: background 0.2s; + display: block !important; + height: 38px !important; + padding: 6px 12px; + font-size: 16px !important; + color: #eee !important; + vertical-align: middle; + background: rgba(255, 255, 255, 0.25); + border: 3px solid rgba(0, 0, 0, 0); + border-radius: 3px; + font-family: inherit; + margin: 0; +} + +textarea, select[multiple] { + height: 114px !important; +} + +div:not(body > div) { + color: rgba(255, 255, 255, 0.45) !important; + display: block; + margin-top: 5px !important; + margin-bottom: 10px !important; + box-sizing: border-box; + font-size: 14px !important; + z-index: 18 !important; +} + +button, input[type="button"i], .button { + padding: 10px 18px !important; + font-size: 16px !important; + line-height: 1.33 !important; + border-radius: 3px; + font-family: inherit; + text-transform: uppercase; + border: 0; + box-shadow: none !important; + position: relative; + overflow: hidden; + color: #fff !important; + background: #cc7b19 !important; + margin-bottom: 0; + font-weight: 400 !important; + vertical-align: middle; + cursor: pointer !important; + white-space: nowrap; + user-select: none; + transition: all 0.1s; +} + +button:hover, input[type="button"i]:hover, .button:hover { + background: #e59029 !important; +} + +[id$="_test"] { + background: #cc7b19 !important; + margin-bottom: 2px; + padding: 10px 8px 10px 10px !important; +} + +[id$="_status"] { + padding: 0 6px !important; + font-size: 16px !important; + border-radius: 3px; + font-family: monospace, sans-serif, sans, arial; + border: 0; + box-shadow: none !important; + color: #fff !important; + background: #666 !important; + border-radius: 4px; +} + +[id$="_status"].false { + background: #cc1b19 !important; +} + +[id$="_status"].true { + background: #7bcc19 !important; +} + +[id$="token"], [data-option$="Token"], [data-option$="API"] { + font-family: monospace, consolas, sans-serif, sans serif, sans, arial; +} + +[id$="-token"]:not(:placeholder-shown), [data-option$="-Token"]:not(:placeholder-shown), [data-option$="-API"]:not(:placeholder-shown) { + text-transform: uppercase; +} + +em { + color: #cc7b19 !important; +} + +select { + margin-left: 10px !important; + font-size: 16px !important; + line-height: inherit; + text-transform: none; +} + +#footer { + text-align: center; + + bottom: 0; + left: 0; + position: fixed; + + width: 100%; +} + +[in-use] { + color: #197bcc; +} + +[in-use]::after { + content: '\2610'; +} + +[in-use="true"i]::after { + content: '\2611'; +} + +[top] { + border-bottom-left-radius: 0 !important; + border-bottom-right-radius: 0 !important; +} + +[bottom] { + border-top-left-radius: 0 !important; + border-top-right-radius: 0 !important; +} + +/* notifications */ +.notification { + background: #F45A26; + border-radius: 4px; + color: #FFF !important; + cursor: pointer; + display: block; + font-family: arial, verdana, sans-serif; + font-size: 20px; + text-align: center; + + position: fixed; + left: 50%; + margin-left: -175px; + padding: 10px; + top: 80px; + + width: 350px; + z-index: 999999; +} + +/* Web to Plex general information notifications */ +.notification.info { + background: #666!important; +} + +/* Web to Plex update notifications */ +.notification.update { + background: #2A2AFF!important; +} + +/* Web to Plex warning notifications */ +.notification.warning { + background: #FF2A2A!important; +} + +/* Web to Plex prompt */ +.prompt { + background: #0008!important; + box-sizing: border-box!important; + color: #eee!important; + display: block!important; + font-family: Open Sans Regular, Helvetica Neue, Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 24px; + overflow: auto; + + height: 100%!important; + width: 100%!important; + + bottom: 0!important; + left: 0!important; + right: 0!important; + top: 0!important; + position: fixed!important; + z-index: 999999999!important; +} + +.prompt-body { + background: #282828; + box-shadow: 0 5px 15px #0008; + display: block; + + left: 20%; + top: 5%; + padding-top: 10px; + padding-bottom: 70px; + position: relative; + + height: 60%; + width: 60%; +} + +.prompt-header, .prompt-footer { + background: #323232; + border: 1px solid #0000; + box-sizing: border-box; + color: #eee; + text-size-adjust: 100%; + + margin-top: 0; + padding: 15px 20px; + position: absolute; + + height: 65px; + width: 100%; + + -webkit-tap-highlight-color: #0000; +} + +.prompt-header { + text-align: left; + border-bottom-color: #222; + border-bottom-width: 1px; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + + top: 0; +} + +.prompt-options { + display: block; + overflow-x: hidden; + overflow-y: auto; + + padding: 12px; + position: relative; + top: 65px; + + max-height: calc(100% - 65px); +} + +.prompt-option { + background: #323232; + border: 1px solid #202020; + border-radius: 3px; + color: #999; + display: block; + text-align: left; + + margin-bottom: 10px; + padding: 10px; + + min-height: 20px; +} + +.prompt-option.mutable { + max-width: 60%; +} + +.prompt-option.mutable > *:last-child { + background: #ffffff40; + border-radius: 3px; + transition: all 0.1s; + + height: 30px; + width: 30px; + + float: right; + margin-right: -9px; + margin-top: -9px; + padding: 0; +} + +.prompt-option.mutable > *:last-child:hover { + background: #ffffff4d; +} + +.prompt-option.mutable > *:last-child::after { + content: '\00d7'; +} + +.prompt-footer { + text-align: right; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + border-top-color: #222; + border-top-width: 1px; + + bottom: 0; +} + +.prompt-input { + float: left; + position: relative; + margin-left: -16px!important; + margin-top: -11px!important; +} + +.prompt-accept, .prompt-decline { + transition: all 0.1s; +} + +.prompt-accept { + background: #cc7b19!important; + margin-left: 5px!important; +} + +.prompt-accept:hover { + background: #e59029!important; +} + +.prompt-decline { + background: #ffffff40!important; +} + +.prompt-decline:hover { + background: #ffffff4d!important; +} + +[type="password"] ~ .hidden-help, .hide { + display: none; +} + +article { + color: #999; + background: rgba(0, 0, 0, 0.15) !important; + width: 35vw !important; + position: relative !important; + padding: 15px !important; +} + +summary, option { + margin-bottom: 20px !important; + padding-left: 0; + list-style: none !important; + margin-top: 0 !important; + color: #999; + cursor: pointer; +} + +select:not([multiple]) > option { + background: url(noise.png), #3f4245 !important; +} + +details:last-child > summary { + margin-bottom: 0 !important; +} + +details[open] > summary, h1, h2, h3, h4, h5, h6 { + color: #cc7b19 !important; + text-shadow: 0 0 5px #000; + + z-index: 999; +} + +.test { + background: #197bcc !important; + font-family: monospace; +} + +.test:hover { + background: #298bdc !important; +} + +/* bbodine @CodePen - https://codepen.io/bbodine1/pen/novBm */ +.checkbox { + width: 80px; + height: 26px; + background: #000; + margin: 15px 0; + position: relative; + border-radius: 50px; + box-shadow: inset 0px 1px 1px rgba(0, 0, 0, 0.5), 0px 1px 0px rgba(255, 255, 255, 0.2); +} + +span.checkbox { + display: inline-block; + + margin: 0; + vertical-align: text-bottom; +} + +.checkbox::after { + content: 'OFF'; + color: #666; + position: absolute; + right: 10px; + z-index: 0; + font: 12px/26px Arial, sans-serif; + font-weight: bold; + text-shadow: 1px 1px 0px rgba(255, 255, 255, 0.15); +} + +.checkbox::before { + content: 'ON'; + color: #cc7b19; + position: absolute; + left: 10px; + z-index: 0; + font: 12px/26px Arial, sans-serif; + font-weight: bold; +} + +.checkbox[prompt-yes]::before { + content: attr(prompt-yes); + text-transform: uppercase; +} + +.checkbox[prompt-no]::after { + content: attr(prompt-no); + text-transform: uppercase; +} + +.checkbox[prompt-size="large"i]::before, .checkbox[prompt-size="large"i]::after { + font-size: 30px !important; +} + +.checkbox[prompt-size="medium"i]::before, .checkbox[prompt-size="medium"i]::after { + font-size: 21px !important; +} + +.checkbox[prompt-size="normal"i]::before, .checkbox[prompt-size="normal"i]::after { + font-size: 12px !important; +} + +.checkbox[prompt-size="small"i]::before, .checkbox[prompt-size="small"i]::after { + font-size: 6px !important; +} + +.checkbox[prompt="y/n"i]::before { + content: 'YES'; +} + +.checkbox[prompt="y/n"i]::after { + content: 'NO'; +} + +.checkbox label { + display: block; + width: 34px; + height: 20px; + cursor: pointer; + position: absolute; + top: 3px; + left: 3px; + z-index: 1; + background: #666; + border-radius: 50px; + transition: all 0.4s ease; + box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.3); +} + +.checkbox input[type=checkbox] { + visibility: hidden; +} + +.checkbox input[type=checkbox]:checked + label { + left: 43px; + background: #cc7b19; +} + +.checkbox[disabled] { + opacity: 0.25 !important; +} + +[white] { + color: #fff!important; +} + +[orange] { + color: #cc7b19!important; +} + +input[type="range"] { + appearance: none; + -webkit-appearance: none; + + background: #0004; + outline: none; + + height: 5px!important; + width: 83%!important; +} + +input[type="range"] + output { + display: inline-block; + position: relative; + width: 7%!important; + color: #cc7b19; + line-height: 20px; + text-align: center; + border-radius: 3px; + background: #000; + padding: 5px 10px; + margin-left: 8px; + vertical-align: sub; +} + +input[type="range"] + output::after { + position: absolute; + top: 8px; + left: -7px; + width: 0; + height: 0; + border-top: 7px solid transparent; + border-right: 7px solid #000; + border-bottom: 7px solid transparent; + content: ''; +} + +input[type="range"]::-webkit-slider-thumb { + appearance: none; + -webkit-appearance: none; + + background: #cc7b19; + border: 1px solid #cc7b19; + border-radius: 100%; + cursor: pointer; + + height: 32px; + width: 32px; +} + +input[type="range"]::-moz-range-thumb { + background: #cc7b19; + border: 1px solid #cc7b19; + border-radius: 100%; + cursor: pointer; + + height: 32px; + width: 32px; +} + +#version { + color: #fff; + + position: fixed; + right: 4px; + top: calc(100vh - 24px); +} + +[disabled], [disabled] * { + cursor: not-allowed!important; + color: #909090EE!important; +} + +[code], code { + border: 1px solid #FFF3; + font-family: monospace, console, consolas, system, arial !important; + + padding: 0 3px; +} + +*::-webkit-scrollbar { + width: 10px; +} + +*::-webkit-scrollbar-thumb { + min-height: 50px; + background: rgba(255, 255, 255, 0.15); + border: 2px solid rgba(0, 0, 0, 0); + border-radius: 8px; + background-clip: padding-box; +} + +*::-webkit-scrollbar-track { + background: url(noise.png) fixed, url(256.png) no-repeat fixed center, #3f4245 !important; +} + +*::placeholder { + color: #999!important; +} + +*::-moz-placeholder, *:-moz-placeholder { + color: #999; +} + +*::-webkit-input-placeholder { + color: #999; +} diff --git a/moz/options.html b/moz/options.html new file mode 100644 index 0000000..41065b5 --- /dev/null +++ b/moz/options.html @@ -0,0 +1,906 @@ + + + + + + Web To Plex | Options + + + + + +
+

Plex Settings

+ +
+
+ Login + +

Login Settings

+
+ Use your Plex token + +
+ How to find your Plex token. +
+
+ +

— OR —

+ +
+ Login with Plex + + +
+ Your Plex username and password are never stored, only your Plex token. +
+ Your username and password are used in order to get a token from Plex itself. +
+
+ +

— OR —

+ +
+ Attach to Ombi* + + +
+

Fill in Manager Settings with Ombi?

+
+ + +
+
+ Your Ombi Plex token will be shared by this extension. +
+
+

+
+ +
+
+ + +
+
+ +
+ Advance + +

Plex Server Options

+
+ + +
+ Use this to communicate directly with your Plex server.
+ Such as http://localhost:32400/ or http://192.168.1.100:32400/ +
+
+
+
+
+ +
+

Manager Settings

+ +
+
+ Ombi (Movies/TV Shows) + +

Connection Settings

+
+ + +
+ Such as https://example.com/ombi or http://192.168.1.100:5000 +
+
+ +
+ + +
+ 1. Go to Ombi | Settings | Ombi
+ 2. Copy/Paste the "API Key."
+ Such as aa756d33242f6g8ffbca2b3963586f21 +
+
+ + +
+ +
+

+
+ try out ombi +
+
+ +
+ +
+ Watcher (Movies) + +

Connection Settings

+
+ + +
+ Such as https://example.com/watcher or http://192.168.1.100:9090 +
+
+ +
+ + +
+ 1. Go to Watcher | Settings | Server
+ 2. Copy/Paste the "API Key."
+ Such as aa756d33242f6g8ffbca2b3963586f21 +
+
+ +

Login (saved)

+
The default username is watcher. The default password is rehctaw.
+
+
+ + +
Only use this if you setup a Watcher username.
+ +
+
+ + +
Only use this if you setup a Watcher password.
+
Your password will be hidden once saved.
+
+
This information will be used for Basic Access Authentication only. This will allow the extension to ask Watcher for your list of films, or to add to your list of films.
+
+ +
+ +
+ +
+ + +
+
+ + +
+

+
+ try out watcher +
+
+ +
+ Radarr (Movies) + +

Connection Settings

+
+ + +
+ Such as https://example.com/radarr or http://192.168.1.100:7878 +
+
+ +
+ + +
+ 1. Go to Radarr | Settings | General
+ 2. Click on "Show advance," then copy/paste the "API Key."
+ Such as aa756d33242f6g8ffbca2b3963586f21 +
+
+ +

Login (saved)

+
+ + +
Only use this if you setup a Radarr username.
+
+
+ + +
Only use this if you setup a Radarr password.
+
Your password will be hidden once saved.
+
+
This information will be used for Basic Access Authentication only. This will allow the extension to ask Radarr for your list of films, or to add to your list of films.
+
+ +
+ +
+ +
+ + +
+ This should be the same path (verbatim) used in Radarr. +
+
+
+ + +
+

+
+ try out radarr +
+
+ +
+ CouchPotato (Movies) + +

Connection Settings

+
+ + +
+ Such as https://example.com/couchpotato or http://192.168.1.100:5050 +
+
+ +
+ + +
+ 1. Go to CouchPotato | Settings
+ 2. Copy/Paste the "API Key."
+ Such as aa756d33242f6g8ffbca2b3963586f21 +
+
+ +

Login (saved)

+
+ + +
Only use this if you setup a CouchPotato username.
+
+
+ + +
Only use this if you setup a CouchPotato password.
+
Your password will be hidden once saved.
+
+ +
+
This information will be used for Basic Access Authentication only. This will allow the extension to ask Couchpotato for your list of videos, or to add to your list of videos.
+
+ +
+ +
+

+
+ try out couchpotato +
+
+ +
+ +
+ Medusa (TV Shows) + +

Connection Settings

+
+ + +
+ Such as https://example.com/medusa or http://192.168.1.100:8081 +
+
+ +
+ + +
+ 1. Go to Medusa | Settings | General | Interface | Web Interface
+ 2. Copy/Paste the "API key."
+ Such as aa756d33242f6g8ffbca2b3963586f21 +
+
+ +

Login (saved)

+
+ + +
+
+ + +
Your password will be hidden once saved.
+
+
This information will be used for Basic Access Authentication only. This will allow the extension to ask Medusa for your list of TV shows, or to add to your list of TV shows.
+
+ +
+ +
+ +
+ + +
+ This should be the same path (verbatim) used in Medusa. +
+
+
+ + +
+ + +
+

+
+ try out medusa +
+
+ +
+ Sonarr (TV Shows) + +

Connection Settings

+
+ + +
+ Such as https://example.com/sonarr or http://192.168.1.100:8989 +
+
+ +
+ + +
+ 1. Go to Sonarr | Settings | General
+ 2. Click on "Show advance," then copy/paste the "API Key."
+ Such as aa756d33242f6g8ffbca2b3963586f21 +
+
+ +

Login (saved)

+
+ + +
Only use this if you setup a Sonarr username.
+
+
+ + +
Only use this if you setup a Sonarr password.
+
Your password will be hidden once saved.
+
+
This information will be used for Basic Access Authentication only. This will allow the extension to ask Sonarr for your list of TV shows, or to add to your list of TV shows.
+
+ +
+ +
+ +
+ + +
+ This should be the same path (verbatim) used in Sonarr. +
+
+
+ + +
+

+
+ try out sonarr +
+
+ +
+ Sick Beard (TV Shows) + +

Connection Settings

+
+ + +
+ Such as https://example.com/sickBeard or http://192.168.1.100:8081 +
+
+ +
+ + +
+ 1. Go to Sick Beard | Config | General | API
+ — a. Ensure the checkbox "Enable API" is enabled
+ — b. Press the "Generate" button
+ 2. Copy/Paste the "API Key."
+ Such as aa756d33242f6g8ffbca2b3963586f21 +
+
+ +

Login (saved)

+
+ + +
+
+ + +
Your password will be hidden once saved.
+
+
This information will be used for Basic Access Authentication only. This will allow the extension to ask Sick Beard for your list of TV shows, or to add to your list of TV shows.
+
+ +
+ +
+ +
+ + +
+ This should be the same path (verbatim) used in Sick Beard. +
+
+
+ + +
+ + + +
+

+
+ try out Sick Beard +
+
+
+
+
+

Other Settings

+ +
+
+ Theme Settings + +

The Button

+
+ Where should the button be? +
+ + + + + +
+
+
+ +
+
+ Site Settings + +

Experimental Sites

+
+ +
+
+ +

Default Sites

+
+ +
+
+
+ +
+
+ Connection Settings + +

Proxy Settings

+
+

+ Force Secure Connections + + + + +

+
+ If enabled, all insecure (HTTP) requests will be through an HTTPS proxy. +
+
+ +
+

Proxy URL & Syntax

+ +
+ Please provide the URL of your proxy. +
+ If your proxy requires a special syntax, enter that information in as well. +
+
    +
  • {raw-url} OR {url} — the raw, uneditied URL
  • +
  • {enc-url} OR {encode-url} — an encoded URL
  • +
  • {b64-url} OR {base64-url} — a Base-64 encoded URL
  • +
+
+
+ +
+

Proxy Headers

+ +
+ If your proxy requires special headers, enter that information in here. +
+
+
+
+ +
+
+ Media Settings +
+

+ Auto Grab + + + + +

+
+ When the user presses the Grab button, the extension should: +
    +
  • Grab ALL: Find items not on Plex, and grab them
  • +
  • ASK user: Find items not on Plex, and grab what the user approves
  • +
+
+

Maximum Auto Grabs

+ + +
+ How many items can be automatically handled before requiring permission to continue? +
+
+
+
+

+ Prompt for Save Location + + + + +

+
+ When the user presses the Grab button should the save location be asked for? +
+ Only supports Medusa, Radarr, and Sonarr. +
+
+
+
+

+ Prompt for Quality + + + + +

+
+ When the user presses the Grab button should the quality be asked for? +
+ Only supports Medusa, Radarr, and Sonarr. +
+
+
+
+ +
+
+ Notification Settings +

+ Ignore Found Items + + + + +

+
+ When the user presses the Grab button and an item already exists, should the notification be ignored or not? +
+
+

+ Ignore Repetitive Notifications + + + + +

+
+ When the user presses the Grab button and there are several incoming notifications of similar information, should the notifications be ignored (after the first one) or not? +
+
+
+ +
+
+ Search Settings +
+

+ Loose Searching + + + + +

+ +
+ + +
+ Allows the extension to search for non-English titles using pattern matching (as a last resort). +
+ Higer sensitivity means more strict searches. +
+
+
+
+

+ Manager Searching + + + + +

+
+ Allows the extension to use your manager(s) to find media.
+ Currently supports: Medusa, Ombi, Radarr, and Sonarr. +
+
+
+
+ +
+
+ Advance Settings +

API Keys

+
+ + +
+ You can sign up for an API key, or have OMDb used as a last resort. +
+
+ +
+ + +
+ You can learn more on how to obtain an API key. +
+
+
+

Data Handling

+
+

Configuration Data (Copy/Paste)

+ +
+ Use this for testing purposes only, as it may contain saved usernames and/or passwords. +
+
+ + +
+
+
+

Cached Data

+ +
+ +
+ This will remove all of your Local Search, permission, and other cached data. +
+ This action cannot be undone. +
+
+
+

Developer Options

+
+

+ Developer Mode + + + + +

+ +
+
+
+ +
+
+ Help + +

External Links

+
+ + +
+ +
+ +
+ Download, and/or find more information for Watcher. +
+
+ +
+ +
+ Download, and/or find more information for Radarr. +
+
+ +
+ +
+ Download, and/or find more information for Sonarr. +
+
+ +
+ +
+ Download, and/or find more information for CouchPotato. +
+
+ +
+ +
+ Download, and/or find more information for Ombi. +
+
+ +
+ +
+ Download, and/or find more information for Medusa. +
+
+ +
+ +
+ Download, and/or find more information for Sick Beard. +
+
+
+
+
+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + diff --git a/moz/options.js b/moz/options.js new file mode 100644 index 0000000..398fc08 --- /dev/null +++ b/moz/options.js @@ -0,0 +1,2057 @@ +/* global parseXML */ +/* Notes: + #1: See https://github.com/SpaceK33z/web-to-plex/commit/db01d1a83d32e4d73f2ea671f634e6cc5b4c0fe7 + #2: See https://github.com/SpaceK33z/web-to-plex/commit/27506b9a4c12496bd7aad6ee09deb8a5b9418cac + #3: See https://github.com/SpaceK33z/web-to-plex/issues/21 + #4: See https://github.com/SpaceK33z/web-to-plex/issues/61 +*/ + +let DEVELOPER_MODE; + +if(chrome.runtime.lastError) + /* Always causes errors on *nix machines, so just "poke" the errors here */ + chrome.runtime.lastError.message; + +// FireFox doesn't support sync storage. +const storage = (chrome.storage.sync || chrome.storage.local), + $ = (selector, all) => (all? document.querySelectorAll(selector): document.querySelector(selector)), + __servers__ = $('[data-option="preferredServer"]'), + __sickBeard_qualityProfile__ = $(`[data-option="sickBeardQualityProfileId"]`), + __sickBeard_storagePath__ = $(`[data-option="sickBeardStoragePath"]`), + __medusa_qualityProfile__ = $(`[data-option="medusaQualityProfileId"]`), + __medusa_storagePath__ = $(`[data-option="medusaStoragePath"]`), + __watcher_qualityProfile__ = $(`[data-option="watcherQualityProfileId"]`), + __watcher_storagePath__ = $(`[data-option="watcherStoragePath"]`), + __radarr_qualityProfile__ = $(`[data-option="radarrQualityProfileId"]`), + __radarr_storagePath__ = $(`[data-option="radarrStoragePath"]`), + __sonarr_qualityProfile__ = $(`[data-option="sonarrQualityProfileId"]`), + __sonarr_storagePath__ = $(`[data-option="sonarrStoragePath"]`), + __save__ = $('#save'), + __options__ = [ + /* Plex Settings */ + 'plexURL', + 'plexToken', + 'UseOmbi', + 'preferredServer', + + /* Manager Settings */ + // Ombi + 'usingOmbi', + 'ombiURLRoot', + 'ombiToken', + + // Medusa + 'usingMedusa', + 'medusaURLRoot', + 'medusaToken', + 'medusaBasicAuthUsername', + 'medusaBasicAuthPassword', + 'medusaStoragePath', + 'medusaQualityProfileId', + + // Watcher + 'usingWatcher', + 'watcherURLRoot', + 'watcherToken', + 'watcherBasicAuthUsername', + 'watcherBasicAuthPassword', + 'watcherStoragePath', + 'watcherQualityProfileId', + + // Radarr + 'usingRadarr', + 'radarrURLRoot', + 'radarrToken', + 'radarrBasicAuthUsername', + 'radarrBasicAuthPassword', + 'radarrStoragePath', + 'radarrQualityProfileId', + + // Sonarr + 'usingSonarr', + 'sonarrURLRoot', + 'sonarrToken', + 'sonarrBasicAuthUsername', + 'sonarrBasicAuthPassword', + 'sonarrStoragePath', + 'sonarrQualityProfileId', + + // Sick Beard + 'usingSickBeard', + 'sickBeardURLRoot', + 'sickBeardToken', + 'sickBeardBasicAuthUsername', + 'sickBeardBasicAuthPassword', + 'sickBeardStoragePath', + 'sickBeardQualityProfileId', + + // CouchPotato + 'enableCouchPotato', + 'usingCouchPotato', + 'couchpotatoURLRoot', + 'couchpotatoToken', + 'couchpotatoBasicAuthUsername', + 'couchpotatoBasicAuthPassword', + // 'couchpotatoQualityProfileId', + + /* Other Settings */ + // Connection settings + 'UseProxy', + 'ProxyURL', + 'ProxyHeaders', + + // Media settings + 'UseAutoGrab', + 'AutoGrabLimit', + 'PromptLocation', + 'PromptQuality', + + // Notification Settings + 'NotifyNewOnly', + 'NotifyOnlyOnce', + + // Search Settings + 'UseLoose', + 'UseLooseScore', + 'ManagerSearch', + + // Advance Settings + 'OMDbAPI', + 'TMDbAPI', + 'DeveloperMode', + + // Hidden values + 'watcherQualities', + 'radarrQualities', + 'sonarrQualities', + 'medusaQualities', + 'sickBeardQualities', + 'watcherStoragePaths', + 'radarrStoragePaths', + 'sonarrStoragePaths', + 'medusaStoragePaths', + 'sickBeardStoragePaths', + '__radarrQuality', + '__sonarrQuality', + '__medusaQuality', + '__sickBeardQuality', + '__radarrStoragePath', + '__sonarrStoragePath', + '__medusaStoragePath', + '__sickBeardStoragePath', + '__domains', + '__caught', + '__theme', + + // Builtins + 'builtin_allocine', + 'builtin_amazon', + 'builtin_couchpotato', + 'builtin_fandango', + 'builtin_flickmetrix', + 'builtin_google', + 'builtin_googleplay', + 'builtin_hulu', + 'builtin_imdb', + 'builtin_justwatch', + 'builtin_letterboxd', + 'builtin_metacritic', + 'builtin_moviemeter', + 'builtin_movieo', + 'builtin_netflix', + 'builtin_plex', + 'builtin_rottentomatoes', + 'builtin_shanaproject', + 'builtin_showrss', + 'builtin_tmdb', + 'builtin_tvmaze', + 'builtin_tvdb', + 'builtin_trakt', + 'builtin_vrv', + 'builtin_verizon', + 'builtin_vudu', + 'builtin_vumoo', + 'builtin_youtube', + 'builtin_itunes', + 'builtin_gostream', + 'builtin_tubi', + 'builtin_webtoplex', + + // Plugins - End of file, before "let empty = ..." + 'plugin_toloka', + 'plugin_shanaproject', + 'plugin_myanimelist', + 'plugin_myshows', + 'plugin_indomovie', + 'plugin_redbox', + 'plugin_kitsu', + ]; + +let PlexServers = [], + ServerID = null, + ClientID = null, + manifest = chrome.runtime.getManifest(), + terminal = // See #3 + (DEVELOPER_MODE = $('[data-option="DeveloperMode"]').checked)? + console: + { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m }; + +chrome.manifest = manifest; + +// Not really important variables +// The "caught" IDs (already asked for in managers) +let __caught = { + imdb: [], + tmdb: [], + tvdb: [], +}, +// The theme classes + __theme = []; + +// create and/or queue a notification +// state = "error" - red +// state = "update" - blue +// state = "info" - grey +// anything else for state will show as orange +class Notification { + constructor(state, text, timeout = 7000, callback = () => {}, requiresClick = true) { + let queue = (Notification.queue = Notification.queue || { list: [] }), + last = queue.list[queue.list.length - 1]; + + if (last && last.done === false) + return (last => setTimeout(() => new Notification(state, text, timeout, callback, requiresClick), +(new Date) - last.start))(last); + + let element = document.furnish(`div.notification.${state}`, { + onclick: event => { + let notification = Notification.queue[event.target.id], + element = notification.element; + + notification.done = true; + Notification.queue.list.splice(notification.index, 1); + clearTimeout(notification.job); + element.remove(); + + let removed = delete Notification.queue[notification.id]; + + return (event.requiresClick)? null: notification.callback(removed); + } + }, text); + + queue[element.id = +(new Date)] = { + start: +element.id, + stop: +element.id + timeout, + span: +timeout, + done: false, + index: queue.list.length, + job: setTimeout(() => element.onclick({ target: element, requiresClick }), timeout), + id: +element.id, + callback, element + }; + queue.list.push(queue[element.id]); + + document.body.appendChild(element); + + return queue[element.id]; + } +} + +class Prompt { + constructor(type, options, callback = () => {}, container = document.body) { + let prompt, remove, create, + array = (options instanceof Array? options: [].slice.call(options)), + data = [...array]; + + switch(type) { + /* Allows the user to add and remove items from a list */ + case 'prompt': + case 'input': + remove = element => { + let prompter = document.querySelector('.prompt'), + header = document.querySelector('.prompt-header'), + counter = document.querySelector('.prompt-options'); + + if(element === true) + return prompter.remove(); + else + element.remove(); + + data.splice(+element.value, 1, null); + header.innerText = 'Approve ' + counter.children.length + (counter.children.length == 1?' item': ' items'); + }; + + prompt = document.furnish('div.prompt', {}, + document.furnish('div.prompt-body', {}, + // The prompt's title + document.furnish('h1.prompt-header', {}, 'Approve ' + array.length + (array.length == 1? ' item': ' items')), + + // The prompt's items + document.furnish('div.prompt-options', {}, + ...(create = ITEMS => { + let elements = []; + + for(let index = 0, length = ITEMS.length, ITEM; index < length; index++) + ITEM = ITEMS[index], + elements.push( + document.furnish('li.prompt-option.mutable', { value: index }, + JSON.stringify(ITEM), + document.furnish('button', { title: 'Remove', onclick: event => { remove(event.target.parentElement); event.target.remove() } }) + ) + ); + + return elements + })(array) + ), + + // The engagers + document.furnish('div.prompt-footer', {}, + document.furnish('input.prompt-input[type=text]', { placeholder: 'Add an item (enter to add)', onkeydown: event => { + let self = event.target; + + if (event.keyCode === 13) { + event.preventDefault(); + remove(true); + + let value = self.value; + + try { + value = JSON.parse(value); + } catch(error) { + /* Suppress input errors */ + } + + new Prompt(type, [value, ...data.filter(value => value !== null && value !== undefined)], callback, container); + } + } }), + document.furnish('button.prompt-decline', { onclick: event => { remove(true); callback([]) } }, 'Close'), + document.furnish('button.prompt-accept', { onclick: event => { remove(true); new Prompt(type, options, callback, container) } }, 'Reset'), + document.furnish('button.prompt-accept', { onclick: event => { remove(true); callback(data.filter(value => value !== null && value !== undefined)) } }, 'Continue') + ) + ) + ); + break; + + /* Allows the user to remove predetermined items */ + case 'select': + remove = element => { + let prompter = document.querySelector('.prompt'), + header = document.querySelector('.prompt-header'), + counter = document.querySelector('.prompt-options'); + + if(element === true) + return prompter.remove(); + else + element.remove(); + + data.splice(+element.value, 1, null); + header.innerText = 'Approve ' + counter.children.length + (counter.children.length == 1?' item': ' items'); + }; + + prompt = document.furnish('div.prompt', {}, + document.furnish('div.prompt-body', {}, + // The prompt's title + document.furnish('h1.prompt-header', {}, 'Approve ' + array.length + (array.length == 1? ' item': ' items')), + + // The prompt's items + document.furnish('div.prompt-options', {}, + ...(create = ITEMS => { + let elements = []; + + for(let index = 0, length = ITEMS.length, ITEM; index < length; index++) + ITEM = ITEMS[index], + elements.push( + document.furnish('li.prompt-option.mutable', { value: index }, + JSON.stringify(ITEM), + document.furnish('button', { title: 'Remove', onclick: event => { remove(event.target.parentElement); event.target.remove() } }) + ) + ); + + return elements + })(array) + ), + + // The engagers + document.furnish('div.prompt-footer', {}, + document.furnish('button.prompt-decline', { onclick: event => { remove(true); callback([]) } }, 'Close'), + document.furnish('button.prompt-accept', { onclick: event => { remove(true); new Prompt(type, options, callback, container) } }, 'Reset'), + document.furnish('button.prompt-accept', { onclick: event => { remove(true); callback(data.filter(value => value !== null && value !== undefined)) } }, 'Continue') + ) + ) + ); + break; + + default: + return terminal.warn(`Unknown prompt type "${ type }"`); + break; + } + + return container.append(prompt), prompt; + } +} + +function load(name) { + return JSON.parse(localStorage.getItem(btoa(name))); +} + +function save(name, data) { + return localStorage.setItem(btoa(name), JSON.stringify(data)); +} + +function getServers(plexToken) { + return fetch('https://plex.tv/api/resources?includeHttps=1', { + headers: { + 'X-Plex-Token': plexToken + } + }) + .then(response => response.text()) + .then(xml => { + let data = parseXML(xml); + + if(/^\s*Invalid/i.test(data)) + return null; + + return data.Device.filter(device => !!~device.provides.split(',').indexOf('server')); + }); +} + +/* See #1 */ +function tryPlexLogin(username, password) { + let hash = btoa(`${username}:${password}`); + + return fetch(`https://plex.tv/users/sign_in.json`, { + method: 'POST', + headers: { + 'X-Plex-Product': 'Web to Plex', + 'X-Plex-Version': manifest.version, + 'X-Plex-Client-Identifier': ClientID, + 'Authorization': `Basic ${ hash }` + } + }) + .then(response => response.json()); +} + +function performPlexLogin() { + let u = $('#plex_username').value, + p = $('#plex_password').value, + s = $('#plex_test_status'); + + s.title = ''; + __servers__.innerHTML = ''; + __save__.disabled = true; + + tryPlexLogin(u, p) + .then(response => { + if(response.error) + return s.title = 'Invalid login information', null; + + if(response.user) { + let t = $('#plex_token'); + + ClientID = t.value = t.textContent = response.user.authToken; + + return performPlexTest(); + } + }); +} + +function performPlexTest(ServerID) { + let plexToken = $('#plex_token').value, + teststatus = $('#plex_test_status'), + inusestataus = [...$('[in-use="plex_token"]', true)]; + + __save__.disabled = true; + __servers__.innerHTML = ''; + teststatus.textContent = '?'; + + getServers(plexToken).then(servers => { + PlexServers = servers || []; + teststatus.textContent = '!'; + inusestataus.map(e => e.setAttribute('in-use', false)); + + if(!servers) + return teststatus.title = 'Failed to communicate with Plex', teststatus.classList = false; + inusestataus.map(e => e.setAttribute('in-use', true)); + + __save__.disabled = false; + teststatus.classList = true; + + (servers = [{ sourceTitle: 'GitHub', clientIdentifier: '', name: 'No Plex Server' }, ...servers]).forEach(server => { + let $option = document.createElement('option'), + source = server.sourceTitle; + + $option.value = server.clientIdentifier; + $option.textContent = `${ server.name }${ source ? ` \u2014 ${ source }` : '' }`; + __servers__.appendChild($option); + }); + + if(ServerID) { + __servers__.value = ServerID; + } + }); +} + +function getPlexConnections(server) { + // `server.Connection` can be an array or object. + let connections = []; + + if(server.Connection instanceof Array) + connections = server.Connection; + else + connections = [server.Connection]; + + return connections.map(connection => ({ + uri: connection.uri, + local: connection.local === '1' + })); +} + +function getOptionValues() { + let options = {}; + + for(let key in __caught) + __caught[key] = __caught[key].filter(id => id).slice(0, 100); + + __theme = __theme.filter(v => v); + + $('[data-option="__caught"i]').value = JSON.stringify(__caught); + $('[data-option="__theme"i]').value = JSON.stringify(__theme); + + __options__.forEach(option => { + let element = $( + `[data-option="${ option }"]` + ); + + if(element) { + if(element.type == 'checkbox') + options[option] = element.checked || element.getAttribute('save') == "true"; + else + options[option] = element.value; + } + }); + + return options; +} + +function performOmbiLogin() { + let l = $('#ombi_url').value, + a = $('#ombi_api').value, + s = $('#plex_test_status'), + e = ($0, $1, $$, $_) => ($1 + (/\\/.test($_)? '\\': '/')); + + l = l + .replace(/([^\\\/])$/, e) + .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); + s.title = ''; + __servers__.innerHTML = ''; + __save__.disabled = true; + + let APIURL = `${ l }api/v1/`, + headers = { headers: { apikey: a, accept: 'application/json' } }; + + fetch(`${ APIURL }Settings/plex`, headers) + .then( response => response.json() ) + .then( json => { + /* Get Plex's details first. If it's disabled, or non-existent, then exit */ + /* Swagger API says "enable", but we'll go with "enabled" */ + if(json && (json.enable || json.enabled)) { + let t = $('#plex_token'), + u = $('[data-option="UseOmbi"]'), + s = __servers__; + + json = (json && json.servers.length? json.servers[0]: {}); + + let name = json.name, // people friendly server name + token = json.plexAuthToken, // the auth token + uuid = json.machineIdentifier, // the machine ID + url = json.ip; // the Plex URL used + + url = url.replace(/(?:[^\/]+\/\/)?([^\/]+)\/?/, `http${ json.ssl? 's': '' }://$1:${ json.port }/`); + + ClientID = t.value = t.textContent = token; + ServerID = s.value = uuid; + s.innerHTML = ``; + + /* Now we can fill in the other details */ + if(u.checked) { + // Ombi + let L = $('[data-option="ombiURLRoot"]'), + A = $('[data-option="ombiToken"]'); + + L.value = L.textContent = l; + A.value = A.textContent = a; + + new Notification('update', 'Filled in Ombi', 3000); + + // CouchPotato + fetch(`${ APIURL }Settings/CouchPotato`, headers) + .then( data => data.json() ) + .then( json => { + if(!json || (!json.enabled && !json.enable)) return; + + let k = $('[data-option="couchpotatoToken"]'), + K = $('[data-option="couchpotatoURLRoot"]'); + + k.value = k.textContent = json.apiKey; + K.value = K.textContent = json.ip.replace(/(?:[^\/]+\/\/)?([^\/]+)\/?/, `http${ json.ssl? 's': '' }://$1:${ json.port }/`); + + new Notification('update', 'Filled in CouchPotato', 3000); + } ) + .catch( error => { new Notification('error', 'Error getting CouchPotato details from Ombi'); throw error } ); + + // Radarr + fetch(`${ APIURL }Settings/radarr`, headers) + .then( data => data.json() ) + .then( json => { + if(!json || (!json.enabled && !json.enable)) return; + + let k = $('[data-option="radarrToken"]'), + K = $('[data-option="radarrURLRoot"]'), + q = $('[data-option="radarrQualityProfileId"]'), + Q = $('[data-option="radarrStoragePath"]'), + _q, _Q; + + k.value = k.textContent = json.apiKey; + K.value = K.textContent = json.ip.replace(/(?:[^\/]+\/\/)?([^\/]+)\/?/, `http${ json.ssl? 's': '' }://$1:${ json.port }/`); + q.value = _q = json.defaultQualityProfile; + Q.value = _Q = json.defaultRootPath; + + q.innerHTML = ``; + Q.innerHTML = ``; + + new Notification('update', 'Filled in Radarr', 3000); + } ) + .catch( error => { new Notification('error', 'Error getting Radarr details from Ombi'); throw error } ); + + // Sonarr + fetch(`${ APIURL }Settings/sonarr`, headers) + .then( data => data.json() ) + .then( json => { + if(!json || (!json.enabled && !json.enable)) return; + + let k = $('[data-option="sonarrToken"]'), + K = $('[data-option="sonarrURLRoot"]'), + q = $('[data-option="sonarrQualityProfileId"]'), + Q = $('[data-option="sonarrStoragePath"]'), + _q, _Q; + + k.value = k.textContent = json.apiKey; + K.value = K.textContent = json.ip.replace(/(?:[^\/]+\/\/)?([^\/]+)\/?/, `http${ json.ssl? 's': '' }://$1:${ json.port }/`); + q.value = _q = json.qualityProfile; + Q.value = _Q = json.rootPath; + + q.innerHTML = ``; + Q.innerHTML = ``; + + new Notification('update', 'Filled in Sonarr', 3000); + } ) + .catch( error => { new Notification('error', 'Error getting Sonarr details from Ombi'); throw error } ); + } + + __save__.disabled = false; + } else { + /* Plex either doesn't exist, or is disabled */ + new Notification('error', 'Error getting Plex details from Ombi'); + } + } ) + .catch( error => { new Notification('error', error); throw error } ); +} + +function performOmbiTest(refreshing = false) { + let options = getOptionValues(), + teststatus = $('#ombi_test_status'), + path = $('[data-option="ombiURLRoot"]'), + url, + headers = { headers: { apikey: options.ombiToken, accept: 'text/html' } }, + enabled = $('#using-ombi'); + + teststatus.textContent = '?'; + options.ombiURLRoot = url = path.value = options.ombiURLRoot.replace(/^(\:\d+)/, 'localhost$1').replace(/^(?!^http(s)?:)/, 'http$1://').replace(/\/+$/, ''); + + let Get = () => { + fetch(`${ url }/api/v1/Request/movie`) + .then(r => r.json()) + .thne(json => { + json.map(item => { + __caught.imdb.push(item.imdbId); + __caught.tmdb.push(item.theMovieDbId); + }); + }); + + fetch(`${ url }/api/v1/Request/tv`) + .then(r => r.json()) + .thne(json => { + json.map(item => { + __caught.imdb.push(item.imdbId); + __caught.tvdb.push(item.tvDbId); + }); + }); + + fetch(`${ url }/api/v1/Status`, headers) + .then( response => response.text() ) + .then( status => { + if (!status || !status.length) throw new Error('Unable to communicate with Ombi'); + + if ((status = +status) >= 200 && status < 400) { + teststatus.textContent = '!'; + enabled.checked = teststatus.classList = true; + enabled.parentElement.removeAttribute('disabled'); + } else { + teststatus.textContent = '!'; + enabled.checked = teststatus.classList = false; + enabled.parentElement.setAttribute('disabled'); + + throw new Error(`Ombi error [${ status }]`); + } + } ) + .catch( error => { new Notification('error', error) } ); + } + + if(refreshing) + Get(); + else if(url && url.length) + requestURLPermissions(url + '/*', allowed => + (allowed)? + Get(): + new Notification('error', 'The user refused permission to access Ombi') + ); +} + +function getWatcher(options, api = "getconfig") { + if(!options.watcherToken) + return new Notification('error', 'Invalid Watcher token'); + + let headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'X-Api-Key': options.watcherToken + }; + + if(options.watcherBasicAuthUsername) + headers.Authorization = `Basic ${ btoa(`${ options.watcherBasicAuthUsername }:${ options.watcherBasicAuthPassword }`) }`; + + return fetch(`${ options.watcherURLRoot }/api/?apikey=${ options.watcherToken }&mode=${ api }&quality=${ options.watcherQualityProfileId || 'Default' }`, { headers }) + .then(response => response.json()) + .catch(error => { + return new Notification('error', 'Watcher failed to connect with error:' + String(error)), + []; + }); +} + +function performWatcherTest(QualityProfileID = 'Default', refreshing = false) { + let options = getOptionValues(), + teststatus = $('#watcher_test_status'), + path = $('[data-option="watcherURLRoot"]'), + storagepath = __watcher_storagePath__, + quality = __watcher_qualityProfile__, + url, + enabled = $('#using-watcher'); + + quality.innerHTML = ''; + teststatus.textContent = '?'; + storagepath.value = '[Empty]'; + options.watcherURLRoot = url = path.value = options.watcherURLRoot.replace(/^(\:\d+)/, 'localhost$1').replace(/^(?!^http(s)?:)/, 'http$1://').replace(/\/+$/, ''); + + let Get = () => { + getWatcher(options, 'liststatus').then(list => { + list.map(item => { + __caught.imdb.push(item.movies.imdbid); + __caught.tmdb.push(item.movies.tmdbid); + }); + }); + + getWatcher(options, 'getconfig').then(configuration => { + if(!configuration || !configuration.response) return new Notification('error', 'Failed to get Watcher configuration'); + + let names = configuration.config.Quality.Profiles, + path = configuration.config.Postprocessing.moverpath, + syntax = path.replace(/\/([\w\s\/\\\{\}]+)$/, '$1'), + profiles = []; + + path = path.replace(syntax, ''); + + for(let name in names) + profiles.push({ + id: name, + name + }); + + teststatus.textContent = '!'; + teststatus.classList = enabled.checked = !!profiles.length; + + if(!profiles.length) + return teststatus.title = 'Failed to communicate with Watcher'; + enabled.parentElement.removeAttribute('disabled'); + + let qualities = []; + profiles.forEach(profile => { + let option = document.createElement('option'); + let { id, name } = profile; + + option.value = id; + option.textContent = name; + qualities.push({ id, name }); + quality.appendChild(option); + }); + + $('[data-option="watcherQualities"i]').value = JSON.stringify(qualities); + + // Because the was reset, the original value is lost. + if(QualityProfileID) + $('[data-option="__radarrQuality"i]').value = quality.value = QualityProfileID; + }); + + let StoragePaths = []; + getRadarr(options, 'rootfolder').then(storagepaths => { + storagepaths.forEach(path => { + let option = document.createElement('option'); + + StoragePaths.push((option.value = option.textContent = path.path).replace(/\\/g, '/')); + storagepath.appendChild(option); + }); + + $('[data-option="radarrStoragePaths"i]').value = JSON.stringify(storagepaths); + + // Because the was reset, the original value is lost. + if(QualityProfileID) + $('[data-option="__sonarrQuality"i]').value = quality.value = QualityProfileID; + }); + + let StoragePaths = []; + getSonarr(options, 'rootfolder').then(storagepaths => { + storagepaths.forEach(path => { + let option = document.createElement('option'); + + StoragePaths.push((option.value = option.textContent = path.path).replace(/\\/g, '/')); + storagepath.appendChild(option); + }); + + $('[data-option="sonarrStoragePaths"i]').value = JSON.stringify(storagepaths); + + // Because the was reset, the original value is lost. + if(QualityProfileID) + $('[data-option="__medusaQuality"i]').value = quality.value = QualityProfileID; + }); + + let StoragePaths = []; + getMedusa(options, 'config').then(configuration => { + let storagepaths = configuration.main.rootDirs.filter(d => d.length > 1); + + if(storagepaths.length < 1) return new Notification('error', 'Medusa has no usable storage paths'); + + storagepaths.forEach(path => { + let option = document.createElement('option'); + + StoragePaths.push((option.value = option.textContent = path).replace(/\\/g, '/').replace(/\/+$/, '')); + storagepath.appendChild(option); + }); + + $('[data-option="medusaStoragePaths"i]').value = JSON.stringify(storagepaths.map(path => ({ path, id: path }))); + + // Because the was reset, the original value is lost. + if(QualityProfileID) + $('[data-option="__sickBeardQuality"i]').value = quality.value = QualityProfileID; + }); + + let StoragePaths = []; + getSickBeard(options, 'sb.getrootdirs').then(configuration => { + let storagepaths = configuration.data.filter(d => +d.valid > 0); + + if(storagepaths.length < 1) return new Notification('error', 'Sick Beard has no usable storage paths'); + + storagepaths = storagepaths.map(path => { + let option = document.createElement('option'); + + StoragePaths.push((path = option.value = option.textContent = path.location).replace(/\\/g, '/').replace(/\/+$/, '')); + storagepath.appendChild(option); + + return path; + }); + + $('[data-option="sickBeardStoragePaths"i]').value = JSON.stringify(storagepaths.map((path, index, array) => ({ path, id: index }))); + + // Because the + + +
+ Run on ${ title } +
+ +
+`; + } + } else { + let title = builtin_array[index], + name = 'builtin_' + title.toLowerCase().replace(/\s+/g, ''), + url = new URL(builtins[title]), + js = name.replace(/^builtin_/i, ''), + o = url.origin, + r = url.host.replace(/^(ww\w+\.)/, ''); + + builtin_sites[r] = o; + + builtinElement.innerHTML += +` +

${ title }

+
+ + +
+
+ Run on ${ title } +
+ +
+`; + } + + // save(`permission:${ r }`, true); + // save(`script:${ r }`, js); + // save(`builtin:${ r }`, true); +} + +save('builtin.sites', builtin_sites); + +$('[id^="builtin_"]', true) + .forEach(element => element.addEventListener('click', event => { + let self = event.target, + bid = self.getAttribute('bid'), + js = self.getAttribute('js'); + + if(self.checked) { + terminal.log(bid, builtin_sites[bid]); + requestURLPermissions(builtin_sites[bid].replace(/https?:\/\/(ww\w+\.)?/i, '*://*.').replace(/\/?$/, '/*'), granted => { + save(`permission:${ bid }`, granted); + save(`script:${ bid }`, granted? js: null); + }); + } else { + save(`permission:${ bid }`, false); + save(`script:${ bid }`, null); + } + + save(`builtin:${ bid }`, true); + }) +); + +// Plugins and their links +let plugins = { + 'Indomovie': ['https://indomovietv.club/', 'https://indomovietv.org/', 'https://indomovietv.net/'], + 'Toloka': 'https://toloka.to/', + 'Shana Project': 'https://www.shanaproject.com/', + 'My Anime List': 'https://myanimelist.net/', + 'My Shows': 'https://myshows.me/', + 'Redbox': 'https://www.redbox.com/', + 'Kitsu': 'https://kitsu.io/', + + // Dont' forget to add to the __options__ array! +}, plugin_array = [], plugin_sites = {}, pluginElement = $('#plugins'); + +for(let plugin in plugins) + plugin_array.push(plugin); +plugin_array = plugin_array.sort((a,b) => { let [x, y] = [a, b].map(v => v.toLowerCase()); return x < y? -1: 1; }); + +for(let index = 0, length = plugin_array.length; pluginElement && index < length; index++) { + let plugin = plugins[plugin_array[index]]; + + if(plugin instanceof Array) { + for(let i = 0, l = plugin.length; i < l; i++) { + let title = plugin_array[index], + name = 'plugin_' + title.toLowerCase().replace(/\s+/g, ''), + url = new URL(plugin[i]), + js = name.replace(/^plugin_/i, ''), + o = url.origin, + r = url.host.replace(/^(ww\w+\.)/, ''); + + plugin_sites[r] = o; + + if(!i) + pluginElement.innerHTML += +` +

${ title }

+
+ + +
+
+ Run on ${ title } +
+ +
+`; + } + } else { + let title = plugin_array[index], + name = 'plugin_' + title.toLowerCase().replace(/\s+/g, ''), + url = new URL(plugins[title]), + js = name.replace(/^plugin_/i, ''), + o = url.origin, + r = url.host.replace(/^(ww\w+\.)/, ''); + + plugin_sites[r] = o; + + pluginElement.innerHTML += +` +

${ title }

+
+ + +
+
+ Run on ${ title } +
+ +
+`; + } +} + +save('optional.sites', plugin_sites); + +$('[id^="plugin_"]', true) + .forEach(element => element.addEventListener('click', event => { + let self = event.target, + pid = self.getAttribute('pid'), + js = self.getAttribute('js'); + + if(self.checked) { + terminal.log(pid, plugin_sites[pid]); + requestURLPermissions(plugin_sites[pid].replace(/https?:\/\/(ww\w+\.)?/i, '*://*.').replace(/\/?$/, '/*'), granted => { + save(`permission:${ pid }`, granted); + save(`script:${ pid }`, granted? js: null); + }); + } else { + save(`permission:${ pid }`, false); + save(`script:${ pid }`, null); + } + + save(`builtin:${ pid }`, false); + }) +); + +let empty = () => {}; + +document.addEventListener('DOMContentLoaded', restoreOptions); +__save__.addEventListener('click', saveOptions); + +$('#plex_test') + .addEventListener('click', event => { + let pt = $('#plex_token').value, + pu = $('#plex_username').value, + pp = $('#plex_password').value, + ou = $('#ombi_url').value, + oa = $('#ombi_api').value; + + if(pt) + performPlexTest(ServerID); + else if(pu && pp) + performPlexLogin(); + else if(ou && oa) + performOmbiLogin(); + }); +$('#watcher_test', true).forEach(element => element.addEventListener('click', event => performWatcherTest())); +$('#radarr_test', true).forEach(element => element.addEventListener('click', event => performRadarrTest())); +$('#sonarr_test', true).forEach(element => element.addEventListener('click', event => performSonarrTest())); +$('#medusa_test', true).forEach(element => element.addEventListener('click', event => performMedusaTest())); +$('#ombi_test', true).forEach(element => element.addEventListener('click', event => performOmbiTest())); +$('#sickBeard_test', true).forEach(element => element.addEventListener('click', event => performSickBeardTest())); +$('#enable-couchpotato', true).forEach(element => element.addEventListener('click', event => enableCouchPotato())); + +/* INPUT | Get the JSON data */ +$('#json_get').addEventListener('click', event => { + let data_container = $('#json_data'), + data = atob((data_container.value || data_container.textContent).replace(/\s*\[.+\]\s*/, '')); + + if(!data) return new Notification('warning', 'The data cannot be blank, null, or undefined'); + + try { + restoreOptions(data); + + new Notification('update', 'Restored configuration data', 3000); + } catch(error) { + new Notification('error', `Error restoring configuration data: ${ error }`); + } +}); + +/* OUTPUT | Set the JSON data */ +$('#json_set').addEventListener('click', event => { + let data_container = $('#json_data'), + data = getOptionValues(); + + data_container.value = data_container.textContent = `[${ (new Date).toString().slice(0, 24) }]${ btoa(JSON.stringify(data)) }`; + + new Notification('info', 'Copy the configuration data somewhere safe, use it to restore your options'); +}); + +/* Erase Cached Searches */ +$('#erase_cache').addEventListener('click', event => { + let options = JSON.stringify(getOptionValues()); + + new Notification('info', 'Clearing...', 3000); + storage.get(null, items => { + for(let item in items) + if(/^~\/cache\//i.test(item)) + storage.remove(item); + }); + + saveOptions(event); +}); + +$('#version') + .innerHTML = `Version ${ manifest.version }`; + +$('[type="range"]', true) + .forEach((element, index, array) => { + let sibling = element.nextElementSibling, + symbol = element.getAttribute('symbol') || ''; + + sibling.value = element.value + symbol; + + element.oninput = (event, self) => (self = event.target).nextElementSibling.value = self.value + (self.getAttribute('symbol') || ''); + }); + +$('.checkbox', true) + .forEach((element, index, array) => { + element.addEventListener('click', event => { + let self = event.target; + + while(!~[...self.classList].indexOf('checkbox') && self.parentElement && self.parentElement != self) + self = self.parentElement; + + if('disabled' in self.attributes) + return event.preventDefault(true); + /* Stop the event from further processing */ + }); + }); + +$('.test', true) + .forEach((element, index, array) => { + element.addEventListener('click', async event => { + event.preventDefault(true); + + let self = event.target; + + await saveOptions(event); + + open(self.href, self.target); + }); + }); + +$('[id^="theme:"i]', true) + .forEach((element, index, array) => { + element.addEventListener('click', async event => { + let self = event.target, + R = RegExp; + + let [a, b] = self.getAttribute('theme').split(/\s*:\s*/).filter(v => v), + value = `${self.id.replace(/^theme:/i, '')}-${b}`; + + if(/^(checkbox)$/i.test(self.type) && (self.checked + '') == a) + __theme.push(value); + else if(/^(text|input|button|\B)$/i.test(self.type) && R(self.value + '', 'i').test(a)) + __theme.push(value); + else + __theme = __theme.filter(v => v != value); + }); + }); + +// CORS exception: SecurityError +// MUST be { window }, never { top } +let { hash } = window.location; + +if(hash.length > 1) + switch(hash = hash.slice(1, hash.length).toLowerCase()) { + case 'save': + setTimeout(async() => { + await saveOptions(); + + window.postMessage({ type: 'INITIALIZE' }); + }, 1000); + break; + + default: + terminal.log(`Unknown event "${ hash }"`); + break; + }; diff --git a/moz/plex$.js b/moz/plex$.js new file mode 100644 index 0000000..f25c05c --- /dev/null +++ b/moz/plex$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'plex' }))(); diff --git a/moz/plex.js b/moz/plex.js new file mode 100644 index 0000000..4a1f09d --- /dev/null +++ b/moz/plex.js @@ -0,0 +1,36 @@ +let script = { + "url": "*://app.plex.tv/desktop#!/server/([a-f\\d]+)/(details|list)\\?*", + + "ready": () => $('.loading').empty, + + "timeout": 5000, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('[data-qa-id$="maintitle"i] *').first, + year = $('[data-qa-id$="secondtitle"i] *').first, + type = script.getType(); + + if(!title || !year || type == 'error') + return 5000; + + title = title.textContent; + year = year.textContent; + + year = +(year || YEAR); + + return { type, title, year }; + }, + + "getType": () => { + let cell = $('[data-qa-id$="celltitle"i]').first; + + if(!cell) + return 'error'; + + if(/seasons?/i.test(cell.textContent)) + return 'show'; + return 'movie'; + }, +}; diff --git a/moz/plex.png b/moz/plex.png new file mode 100644 index 0000000..d8c4d9f Binary files /dev/null and b/moz/plex.png differ diff --git a/moz/plexit.16.png b/moz/plexit.16.png new file mode 100644 index 0000000..1661033 Binary files /dev/null and b/moz/plexit.16.png differ diff --git a/moz/plexit.48.png b/moz/plexit.48.png new file mode 100644 index 0000000..d7b31ad Binary files /dev/null and b/moz/plexit.48.png differ diff --git a/moz/plugin.indomovietv.js b/moz/plugin.indomovietv.js new file mode 100644 index 0000000..63ea78d --- /dev/null +++ b/moz/plugin.indomovietv.js @@ -0,0 +1,59 @@ +let plugin = { + "url": "*://*.indomovietv.*/(?!tag|$)", + // TLD changes often: net, org + + "ready": () => !$('[itemprop="name"i]:not(meta), [itemprop="datePublished"i]').empty, + + "timeout": 1000, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('[itemprop="name"i]:not(meta)').first, + year = $('[itemprop="datePublished"i]').first, + image = $('[itemprop="image"i]').first, + type = 'movie'; + + title = title.textContent; + year = +year.textContent.replace(/[^]*(\d{4})[^]*/, '$1'); + image = image.src; + + // auto-prompt downloading for the user + let links = $('[class~="idtabs"i] [href^="#div"i]'); + + if(links.length > 1) { + OLOAD_EVENTS.push(setTimeout( + () => Notify('update', 'Finding download links...', 3000), + 500 + )); + + links.forEach((link, index, array) => OLOAD_EVENTS.push(setTimeout( + () => { + link.click(); + + if(index == links.length -1) + OLOAD_EVENTS.push(setTimeout( + () => Notify('update', 'No download links found'), + 7000 + )); + }, + index * 4500 + ))); + } + + return { type, title, year, image }; + }, +}, + OLOAD_EVENTS = []; + +top.addEventListener('message', request => { + try { + request = request.data; + + if(request) + if(request.from == 'oload' || request.found == true) + OLOAD_EVENTS.forEach(timeout => clearTimeout(timeout)); + } catch(error) { + throw error; + } +}); diff --git a/moz/plugin.kitsu.js b/moz/plugin.kitsu.js new file mode 100644 index 0000000..c55730f --- /dev/null +++ b/moz/plugin.kitsu.js @@ -0,0 +1,36 @@ +// Web to Plex - Kitsu Plugin +// Aurthor(s) - @ephellon (2019) +let plugin = { + "url": "*://*.kitsu.io/anime/*", + + "ready": () => !$('img[data-src][src]').empty, + + "timeout": 1000, + + "init": () => { + let _title = /^\s*(?:english|romanized)\s+(.+)\s*$/i, + _year = /^\s*aired\s+.+(\d{4})(?:\s+to.+)?\s*$/i; + + let title = $('.media--information li').filter(e => _title.test(e.textContent))[0], + year = $('.media--information li').filter(e => _year.test(e.textContent))[0], + image = $('.media-poster img').first, + type = plugin.getType(); + + title = title.textContent.replace(_title, '$1'); + year = +year.textContent.replace(_year, '$1'); + image = image.src; + + return { + type, + title, + year, + image + }; + }, + + "getType": () => { + $('.media--information li').filter(e => /^\s*type\s+(movie|tv([\s\-]?show)?)\s*$/i.test(e.textContent)); + + return /tv/i.test(RegExp.$1)? 'show': 'movie'; + }, +}; diff --git a/moz/plugin.myanimelist.js b/moz/plugin.myanimelist.js new file mode 100644 index 0000000..aa36851 --- /dev/null +++ b/moz/plugin.myanimelist.js @@ -0,0 +1,29 @@ +// Web to Plex - My Anime List Plugin +// Aurthor(s) - @ephellon (2018) + +let plugin = { + "url": "*://*.myanimelist.net/anime/\\d+/*", + + "init": () => { + let title = document.queryBy('table h2:nth-of-type(1) + *') + .first.textContent.replace(/^[^\:]+:/, '') + .trim(), + type = document.queryBy('table h2:nth-of-type(2) + *') + .first.textContent.trim() + .toLowerCase() + .split(/\s+/) + .reverse()[0], + year = +(document.queryBy('table h2:nth-of-type(2) ~ .spaceit ~ .spaceit') + .first.textContent.trim() + .replace(/[^]*(\d{4})[^]*/, '$1')), + image = document.queryBy('table img') + .first.src; + + return { + type, + title, + year, + image + }; + }, +}; diff --git a/moz/plugin.myshows.js b/moz/plugin.myshows.js new file mode 100644 index 0000000..f3946ac --- /dev/null +++ b/moz/plugin.myshows.js @@ -0,0 +1,29 @@ +// Web to Plex - My Shows Plugin +// Aurthor(s) - @enchained (2018) + +let plugin = { + "url": "*://*.myshows.me/view/\\d+/*", + + "init": () => { + let specific = /\/\/(\w{2})\./.test(location.origin); + + let title = ( + specific ? + document.queryBy('[itemprop="name"]').first.textContent: + document.queryBy('main > h1').first.textContent + ) + .trim(), + year = +(document.queryBy('div.clear > p.flat') + .first.textContent.trim() + .replace(/[^]*?(\d{4})[^]*/, '$1')), + IMDbID = document.queryBy('[href*="/title/tt"]') + .first.href.replace(/[^]*(tt\d+)[^]*/, '$1'); + + return { + type: 'show', + title, + year, + IMDbID + }; + }, +}; diff --git a/moz/plugin.redbox.js b/moz/plugin.redbox.js new file mode 100644 index 0000000..1724a3f --- /dev/null +++ b/moz/plugin.redbox.js @@ -0,0 +1,28 @@ +let plugin = { + "url": "*://*.redbox.com/(ondemand-)?(movies|tvshows)/(?!featured|$)", + + "ready": () => !$('[data-test-id$="-name"i]').empty, + + "timeout": 1000, + + "init": (ready) => { + let R = RegExp; + + let title = $('[data-test-id$="-name"i]').first, + year = $('[data-test-id$="-info"i]').first, + image = $('[data-test-id$="-img"i]').first, + type = plugin.getType(); + + title = title.textContent.replace(/\s*\((\d{4})\)/, ''); + year = +(R.$1 || year.textContent.split(/\s*\|\s*/)[1]); + image = image.src; + + return { type, title, year, image }; + }, + + "getType": () => { + return /\bmovies\b/.test(location.pathname)? + 'movie': + 'show'; + }, +}; diff --git a/moz/plugin.shanaproject.js b/moz/plugin.shanaproject.js new file mode 100644 index 0000000..2d47688 --- /dev/null +++ b/moz/plugin.shanaproject.js @@ -0,0 +1,23 @@ +// Web to Plex - Shana Project Plugin +// Aurthor(s) - @ephellon (2018) +let plugin = { + "url": "*://*.shanaproject.com/series/\\d+", + + "init": () => { + let title = $('.overview i, #header_big .header_info_block') + .first.textContent.trim(), + year = +$('#header_big .header_info_block + *') + .first.textContent.trim() + .replace(/[^]*(\d{4})[^]*/m, '$1'), + image = $('#header_big .header_display_box') + .first.style['background-image'].trim() + .replace(/url\((.+)\)/i, '$1'); + + return { + type: 'show', + title, + year, + image + }; + }, +}; diff --git a/moz/plugin.toloka.js b/moz/plugin.toloka.js new file mode 100644 index 0000000..db5f6dd --- /dev/null +++ b/moz/plugin.toloka.js @@ -0,0 +1,53 @@ +// Web to Plex - Toloka Plugin +// Aurthor(s) - @chmez (2017) +/* Minimal Required Layout * + plugin { + url: string, + init: function => ({ type:string, title:string, year:number|null|undefined }) + } +*/ +// REQUIRED [plugin:object]: The plugin object +let plugin = { + // REQUIRED [plugin.url]: this is what you ask Web to Plex access to; currently limited to a single domain + "url": "*://*.toloka.to/*", + + // REQUIRED [plugin.init]: this is what Web to Plex will call on when the url is detected + // it will always be fired after the page and Web to Plex have been loaded + "init": () => { + let title = document.queryBy('.maintitle') + .first.textContent.replace(/^.+\/(.+?)\(([\d]{4})\)\s*$/, '$1') + .trim(), + // REQUIRED [title:string] + // you have access to the exposed "helper.js" file within the extension + + year = +RegExp.$2, + // PREFERRED [year:number, null, undefined] + + image = document.queryBy('.postbody img') + .first.src, + // OPTIONAL [image:string] + + IMDbID = plugin.getID(); + + // the rest of the code is up to you, but should be limited to a layout similar to this + // REQUIRED [{ type:'movie', 'show'; title:string; year:number }] + // PREFERRED [{ image:string; IMDbID:string; TMDbID:string, number; TVDbID:string, number }] + return { + type: 'movie', + title, + year, + image, + IMDbID + }; + }, + + // OPTIONAL: the rest of this code is purely for functionality + "getID": () => { + let links = document.queryBy('.postlink'), + regex = /^https?\:\/\/(?:w{3}\.)?imdb\.com\/title\/(tt\d+)/i; + + for(let link in links) + if(regex.test(links[link])) + return RegExp.$1; + } +}; diff --git a/moz/plugn.js b/moz/plugn.js new file mode 100644 index 0000000..432ef92 --- /dev/null +++ b/moz/plugn.js @@ -0,0 +1,779 @@ +/* plugn.js (Plugin) - Web to Plex */ +/* global chrome */ + +let PLUGN_DEVELOPER = false; + +let PLUGN_TERMINAL = + PLUGN_DEVELOPER? + console: + { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m }; + +let LAST, LAST_JS, LAST_INSTANCE, LAST_ID, LAST_TYPE, FOUND = {}; + +let PLUGN_STORAGE = browser.storage.sync || browser.storage.local; +let PLUGN_CONFIGURATION; + + +let URLRegExp = ` + .replace(/^\\*\\:/,'\\\\w{3,}:') + // *:// + .replace(/\\*\\./g,'(?:[^\\\\.]+\\\\.)?') + // *. + .replace(/\\.\\*/g,'(?:\\\\.[^\\\\/\\\\.]+)?') + // .* + .replace(/([\\/\\?\\&\\#])\\*/g,'$1[^$]*'),'i') + // /* OR ?* OR &* OR #* +`; + +function load(name, private) { + return JSON.parse((private && sessionStorage? sessionStorage: localStorage).getItem(btoa(name))); +} + +function save(name, data, private) { + return (private && sessionStorage? sessionStorage: localStorage).setItem(btoa(name), JSON.stringify(data)); +} + +async function Load(name = '') { + if(!name) + return /* invalid name */; + + name = '~/cache/' + (name.toLowerCase().replace(/\s+/g, '_')); + + return new Promise((resolve, reject) => { + function LOAD(DISK) { + let data = JSON.parse(DISK[name] || null); + + return resolve(data); + } + + PLUGN_STORAGE.get(null, DISK => { + if(browser.runtime.lastError) + browser.runtime.lastError.message || + browser.storage.local.get(null, LOAD); + else + LOAD(DISK); + }); + }); +} + +async function Save(name = '', data) { + if(!name) + return /* invalid name */; + + name = '~/cache/' + (name.toLowerCase().replace(/\s+/g, '_')); + data = JSON.stringify(data); + + await PLUGN_STORAGE.set({[name]: data}, () => data); + + return name; +} + +function GetConsent(name, builtin) { + /* The configuration variable could fail to be initialized */ + if(!PLUGN_CONFIGURATION) + throw 'Configuration not found, exiting prematurely'; + + return PLUGN_CONFIGURATION[`${ (builtin? 'builtin': 'plugin') }_${ name }`]; +} + +async function GetAuthorization(name) { + let authorized = await Load(`has/${ name }`), + permissions = await Load(`get/${ name }`), + Ausername, Apassword, Atoken, + Aapi, Aserver, Aurl, Astorage, + Acache; + + if(!permissions) + return {}; + + function WriteOff(permission) { + if(/^(usernames?)$/i.test(permission)) + Ausername = true; + else if(/^(passwords?)$/i.test(permission)) + Apassword = true; + else if(/^(tokens?)$/i.test(permission)) + Atoken = true; + else if(/^(api)$/i.test(permission)) + Aapi = true; + else if(/^(servers?)$/i.test(permission)) + Aserver = true; + else if(/^(url(?:root)?)$/i.test(permission)) + Aurl = true; + else if(/^(storage)$/i.test(permission)) + Astorage = true; + else if(/^(cache)$/i.test(permission)) + Acache = true; + } + + if(permissions.constructor === Array) + for(let permission of permissions) + WriteOff(permission); + else if(permissions.constructor === Object) + for(let permission in permissions) + WriteOff(permission); + + return { authorized, Ausername, Apassword, Atoken, Aapi, Aserver, Aurl, Astorage, Acache }; +} + +// get the saved options +function getConfiguration() { + return new Promise((resolve, reject) => { + function handleConfiguration(options) { + if((!options.plexToken || !options.servers) && !options.IGNORE_PLEX) + return reject(new Error('Required options are missing')), + null; + + let server, o; + + if(!options.IGNORE_PLEX) { + // For now we support only one Plex server, but the options already + // allow multiple for easy migration in the future. + server = options.servers[0]; + o = { + server: { + ...server, + // Compatibility for users who have not updated their settings yet. + connections: server.connections || [{ uri: server.url }] + }, + ...options + }; + + options.plexURL = o.plexURL? + `${ o.plexURL }web#!/server/${ o.server.id }/`: + `https://app.plex.tv/web/app#!/server/${ o.server.id }/`; + } else { + o = options; + } + + if(o.couchpotatoBasicAuthUsername) + o.couchpotatoBasicAuth = { + username: o.couchpotatoBasicAuthUsername, + password: o.couchpotatoBasicAuthPassword + }; + + // TODO: stupid copy/pasta + if(o.watcherBasicAuthUsername) + o.watcherBasicAuth = { + username: o.watcherBasicAuthUsername, + password: o.watcherBasicAuthPassword + }; + + if(o.radarrBasicAuthUsername) + o.radarrBasicAuth = { + username: o.radarrBasicAuthUsername, + password: o.radarrBasicAuthPassword + }; + + if(o.sonarrBasicAuthUsername) + o.sonarrBasicAuth = { + username: o.sonarrBasicAuthUsername, + password: o.sonarrBasicAuthPassword + }; + + if(o.usingOmbi && o.ombiURLRoot && o.ombiToken) { + o.ombiURL = o.ombiURLRoot; + } else { + delete o.ombiURL; // prevent variable ghosting + } + + if(o.usingCouchPotato && o.couchpotatoURLRoot && o.couchpotatoToken) { + o.couchpotatoURL = `${ items.couchpotatoURLRoot }/api/${encodeURIComponent(o.couchpotatoToken)}`; + } else { + delete o.couchpotatoURL; // prevent variable ghosting + } + + if(o.usingWatcher && o.watcherURLRoot && o.watcherToken) { + o.watcherURL = o.watcherURLRoot; + } else { + delete o.watcherURL; // prevent variable ghosting + } + + if(o.usingRadarr && o.radarrURLRoot && o.radarrToken) { + o.radarrURL = o.radarrURLRoot; + } else { + delete o.radarrURL; // prevent variable ghosting + } + + if(o.usingSonarr && o.sonarrURLRoot && o.sonarrToken) { + o.sonarrURL = o.sonarrURLRoot; + } else { + delete o.sonarrURL; // prevent variable ghosting + } + + resolve(o); + } + + PLUGN_STORAGE.get(null, options => { + if(browser.runtime.lastError) + browser.runtime.lastError.message || + browser.storage.local.get(null, handleConfiguration); + else + handleConfiguration(options); + }); + }); +} + +// self explanatory, returns an object; sets the configuration variable +function parseConfiguration() { + return getConfiguration().then(options => { + PLUGN_CONFIGURATION = options; + + if((PLUGN_DEVELOPER = options.DeveloperMode) && !parseConfiguration.gotConfig) { + parseConfiguration.gotConfig = true; + PLUGN_TERMINAL = + PLUGN_DEVELOPER? + console: + { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m }; + + PLUGN_TERMINAL.warn(`PLUGN_DEVELOPER: ${PLUGN_DEVELOPER}`); + } + + return options; + }, error => { throw error }); +} + +browser.storage.onChanged.addListener(async(changes, namespace) => { + await parseConfiguration(); +}); + +(async() => { + await parseConfiguration(); +})(); + +function RandomName(length = 16, symbol = '') { + let values = []; + + window.crypto.getRandomValues(new Uint32Array(length)).forEach((value, index, array) => values.push(value.toString(36))); + + return values.join(symbol).replace(/^[^a-z]+/i, ''); +}; + +function prepare(code, alias, type) { + + let DATE = (new Date), + YEAR = DATE.getFullYear(), + MONT = DATE.getMonth(), + DAY = DATE.getDate(); + + return `let DATE = (new Date), + YEAR = ${YEAR}, + MONT = ${MONT}, + DAY = ${DAY}; + +` + code +.replace(/\/\/+\s*"([^\"\n\f\r\v]+?)"\s*requires?\:?\s*(.+)([^]+)/i, ($0, $1, $2, $3, $$, $_) => ` +${ $3 } +;(async() => await Require("${ $2 }", "${ alias }", "${ $1 }"))(); +`) + ; +} + +let handle = async(results, tabID, instance, script, type) => { + let InstanceWarning = `[${ type.toUpperCase() }:${ script }] Instance failed to execute @${ tabID }#${ instance }`, + InstanceType = type; + + results = await results; + + if(browser.runtime.lastError) + browser.runtime.lastError.message; + + if((!results || !results[0] || !instance) && !FOUND[instance]) + try { + instance = RandomName(); + tabchange([ TAB ]); + return; + } catch(error) { + return PLUGN_TERMINAL.warn(InstanceWarning); + } + + let data = await results[0]; + + if(typeof data == 'number') { + if(handle.timeout) + return /* already running */; + if(data < 0) + return browser.tabs.sendMessage(tabID, { data, instance, [InstanceType.toLowerCase()]: script, instance_type: InstanceType, type: 'NO_RENDER' }) + /* stop execution and timeouts/intervals */; + + return handle.timeout = setTimeout(() => { let { request, sender, callback } = (processMessage.properties || {}); handle.timeout = null; processMessage(request, sender, callback) }, data); + } else if(typeof data == 'string') { + let R = RegExp; + + if(/^<([^<>]+)>$/.test(data)) + return PLUGN_TERMINAL.warn(`The instance requires the "${ R.$1 }" permission: ${ instance }`); + + data.replace(/^([^]+?)\s*\((\d{4})\):([\w\-]+)$/); + + let title = R.$1, + year = R.$2, + type = R.$3; + + data = { type, title, year }; + } + + if(typeof data == 'number') + return setTimeout(() => { let { request, sender, callback } = (processMessage.properties || {}); processMessage(request, sender, callback) }, data); + if(typeof data != 'object') + return /* setTimeout */; + + try { + if(data instanceof Array) { + data = data.filter(d => d); + + if(data.length > 1) { + browser.tabs.sendMessage(tabID, { data, instance, [InstanceType.toLowerCase()]: script, instance_type: InstanceType, type: 'POPULATE' }); + return /* done */; + } + + /* the array is too small to parse, set it as a single item */ + data = data[0]; + } + + let { type, title, year } = data; + + title = title + .replace(/[\u2010-\u2015]/g, '-') // fancy hyphen + .replace(/[\u201a\u275f]/g, ',') // fancy comma + .replace(/[\u2018\u2019\u201b\u275b\u275c]/g, "'") // fancy apostrophe + .replace(/[\u201c-\u201f\u275d\u275e]/g, '"'); // fancy quotation marks + year = +year; + + data = { ...data, type, title, year }; + + browser.tabs.insertCSS(tabID, { file: 'common.css' }); + browser.tabs.sendMessage(tabID, { + data, + instance, + [InstanceType.toLowerCase()]: script, + instance_type: InstanceType, + type: 'POPULATE' + }).then(response => { + if(browser.runtime.lastError) + browser.runtime.lastError.message; + + PLUGN_TERMINAL.warn('Response [plugn]: ' + JSON.stringify(response)); + + if(!response) + PLUGN_TERMINAL.warn(`Terminated execution, response: ${ JSON.stringify(response) }`); + }); + } catch(error) { + throw new Error(`${ InstanceWarning } - ${ String(error) }`); + } +}; + +let running = [], instance = RandomName(), TAB, cache = {}; + +/* Handle script/plugin events */ +let tabchange = async tabs => { + let tab = tabs[0]; + + if(!tab || FOUND[instance]) return; + + TAB = tab; + + let id = tab.id, + url = tab.url, + org, ali, js, + type, cached, + allowed; + + if( + !url + || /^(?:chrome|debugger|view-source)/i.test(url) + // || (!!~running.indexOf(id) && !!~running.indexOf(instance)) + ) + return /* + Stop if: + a) There isn't a url + b) The url is a chrome url + c) The tab AND instance are accounted for + */; + + url = new URL(url); + org = url.origin; + ali = url.host.replace(/^(ww\w+\.|\w{2}\.)/i, ''); + type = (load(`builtin:${ ali }`) + '') == 'true'? 'script': 'plugin'; + js = load(`${ type }:${ ali }`); + code = cache[ali]; + allowed = await GetConsent(ali, type == 'script'); + + if(!allowed || !js) return; + + let { authorized, ...A } = await GetAuthorization(js); + + if(code) { + browser.tabs.executeScript(id, { file: 'helpers.js' }, () => { + // Sorry, but the instance needs to be callable multiple times + browser.tabs.executeScript(id, { code }, results => handle(results, id, instance, js, type)); + }); + + return setTimeout(() => cache = {}, 1e6); + } + + let name = (!PLUGN_DEVELOPER? instance: `top.${ instance }`), // makes debugging easier + topmost = !/^top\./.test(name); + + let file = (PLUGN_DEVELOPER)? + (type === 'script')? + browser.runtime.getURL(`${ js }.js`): + browser.runtime.getURL(`plugin.${ js }.js`): + `https://ephellon.github.io/web.to.plex/${ type }s/${ js }.js`; + + await fetch(file, { mode: 'cors' }) + .then(response => response.text()) + .then(async code => { + await browser.tabs.executeScript(id, { file: 'helpers.js' }, async() => { + // Sorry, but the instance needs to be callable multiple times + await browser.tabs.executeScript(id, { code: + (LAST = cache[ali] = +`/* ${ type }* (${ (!PLUGN_DEVELOPER? 'on':'off') }line) - "${ url.href }" */ +${ topmost? 'var ': '' }${ name } = (${ name } || (${ name }$ = $ => { +'use strict'; + +/* Required permissions */ +if(${ allowed } === false) + return ''; +if(${ authorized } === false) + return ''; +${ + (() => { + let o = []; + + for(let a in A) + o.push( +`if(${ A[a] } === false) + return '<${ a.slice(1) }>'; +` + ); + + return o.join(''); + })() +} +/* Start Injected */ +${ prepare(code, js, type) } +/* End Injected */ + +let InjectedReadyState; + +top.addEventListener('popstate', ${ type }.init); +top.addEventListener('pushstate-changed', ${ type }.init); + +return (${ type }.RegExp = RegExp( + ${ type }.url +${ URLRegExp } +).test +(location.href)? +/* URL matches pattern */ + ${ type }.ready? + /* Injected file has the "ready" property */ + (InjectedReadyState = + ${ type }.ready.constructor.name == 'AsyncFunction'? + /* "ready" is an async function */ + ${ type }.ready(): + /* "ready" is a sync (normal) function */ + ${ type }.ready() + )? + /* Injected file is ready */ + ${ type }.init( InjectedReadyState ): + /* Injected file isn't ready */ + (${ type }.timeout || 1000): + /* Injected file doesn't have the "ready" property */ + ${ type }.init(): +/* URL doesn't match pattern */ +(console.warn("The domain '${ org }' (" + location.href + ") does not match the domain pattern '" + ${ type }.url + "' (" + ${ type }.RegExp + ")"), -1); +})(document.queryBy)); + +console.log('[${ name.replace(/^(top\.)?(\w{7}).*$/i, '$1$2') }]', ${ name }); + +top.onlocationchange = (event) => browser.runtime.sendMessage({ type: '$INIT$', options: { ${ type }: '${ js }' } }); + +;${ name };` + ) }, results => handle(results, LAST_ID = id, LAST_INSTANCE = instance, LAST_JS = js, LAST_TYPE = type)) + }) + }) + .then(() => running.push(id, instance)) + .catch(error => { throw error }); +}; + +// listen for message event +let processMessage; + +browser.runtime.onMessage.addListener(processMessage = async(request, sender, callback) => { + let { options } = request, + tab = TAB || {}, + { id, url, href } = tab, + org; + + processMessage.properties = { request, sender, callback }; + + if( + !url + || /^(?:chrome|debugger|view-source)/i.test(url) + // || (!!~running.indexOf(id) && !!~running.indexOf(instance)) + ) + return /* + Stop if: + a) There isn't a url + b) The url is a chrome url + c) The tab AND instance are accounted for + */; + + url = new URL(url); + org = url.origin; + + let name = (!PLUGN_DEVELOPER? instance: `top.${ instance }`), // makes debugging easier + topmost = !/^top\./.test(name); + + if(request && request.options) { + let { type } = request, + { plugin, script } = options, + _type = type.toLowerCase(), + allowed; + + type = type.toUpperCase(); + + let file = (PLUGN_DEVELOPER)? + (_type === 'script')? + browser.runtime.getURL(`${ script }.js`): + browser.runtime.getURL(`plugin.${ plugin }.js`): + `https://ephellon.github.io/web.to.plex/${ _type }s/${ options[_type] }.js`; + + let { authorized, ...A } = await GetAuthorization(options[_type]); + + switch(type) { + case 'PLUGIN': + allowed = await GetConsent(plugin, false); + + await fetch(file, { mode: 'cors' }) + .then(response => response.text()) + .then(async code => { + await browser.tabs.executeScript(id, { file: 'helpers.js' }, async() => { + // Sorry, but the instance needs to be callable multiple times + await browser.tabs.executeScript(id, { code: + (LAST = cache[plugin] = +`/* plugin (${ (!PLUGN_DEVELOPER? 'on':'off') }line) - "${ url.href }" */ +${ topmost? 'var ': '' }${ name } = (${ name } || (${ name }$ = $ => { +'use strict'; + +/* Required permissions */ +if(${ allowed } === false) + return ''; +if(${ authorized } === false) + return ''; +${ + (() => { + let o = []; + + for(let a in A) + o.push( +`if(${ A[a] } === false) + return '<${ a.slice(1) }>'; +` + ); + + return o.join(''); + })() +} +/* Start Injected (Plugin) */ +${ prepare(code, plugin, _type) } +/* End Injected */ + +let PluginReadyState; + +top.addEventListener('popstate', plugin.init); +top.addEventListener('pushstate-changed', plugin.init); + +return (plugin.RegExp = RegExp( + plugin.url +${ URLRegExp } +).test +(location.href)? +/* URL matches pattern */ + plugin.ready? + /* Plugin has the "ready" property */ + (PluginReadyState = + plugin.ready.constructor.name == 'AsyncFunction'? + /* "ready" is an async function */ + plugin.ready(): + /* "ready" is a sync (normal) function */ + plugin.ready() + )? + /* Plugin is ready */ + plugin.init( PluginReadyState ): + /* Script isn't ready */ + (plugin.timeout || 1000): + /* Plugin doesn't have the "ready" property */ + plugin.init(): +/* URL doesn't match pattern */ +(console.warn("The domain '${ org }' (" + location.href + ") does not match the domain pattern '" + plugin.url + "' (" + plugin.RegExp + ")"), -1); +})(document.queryBy)); + +console.log('[${ name.replace(/^(top\.)?(\w{7}).*$/i, '$1$2') }]', ${ name }); + +top.onlocationchange = (event) => browser.runtime.sendMessage({ type: '$INIT$', options: { plugin: '${ plugin }' } }); + +;${ name };` +) }, results => handle(results, LAST_ID = id, LAST_INSTANCE = instance, LAST_JS = plugin, LAST_TYPE = type)) + }) + }) + .then(() => running.push(id, instance)) + .catch(error => { throw error }); + break; + + case 'SCRIPT': + allowed = await GetConsent(script, true); + + await fetch(file, { mode: 'cors' }) + .then(response => response.text()) + .then(async code => { + await browser.tabs.executeScript(id, { file: 'helpers.js' }, async() => { + // Sorry, but the instance needs to be callable multiple times + await browser.tabs.executeScript(id, { code: + (LAST = cache[script] = +`/* script (${ (!PLUGN_DEVELOPER? 'on':'off') }line) - "${ url.href }" */ +${ topmost? 'var ': '' }${ name } = (${ name } || (${ name }$ = $ => { +'use strict'; + +/* Required permissions */ +if(${ allowed } === false) + return ''; +if(${ authorized } === false) + return ''; +${ + (() => { + let o = []; + + for(let a in A) + o.push( +`if(${ A[a] } === false) + return '<${ a.slice(1) }>'; +` + ); + + return o.join(''); + })() +} +/* Start Injected (Script) */ +${ prepare(code, script, _type) } +/* End Injected */ + +let ScriptReadyState; + +top.addEventListener('popstate', script.init); +top.addEventListener('pushstate-changed', script.init); + +return (script.RegExp = RegExp( + script.url +${ URLRegExp } +).test +(location.href)? +/* URL matches pattern */ + script.ready? + /* Script has the "ready" property */ + (ScriptReadyState = + script.ready.constructor.name == 'AsyncFunction'? + /* "ready" is an async function */ + script.ready(): + /* "ready" is a sync (normal) function */ + script.ready() + )? + /* Script is ready */ + script.init( ScriptReadyState ): + /* Script isn't ready */ + (script.timeout || 1000): + /* Script doesn't have the "ready" property */ + script.init(): +/* URL doesn't match pattern */ +(console.warn("The domain '${ org }' (" + location.href + ") does not match the domain pattern '" + script.url + "' (" + script.RegExp + ")"), -1); +})(document.queryBy)); + +console.log('[${ name.replace(/^(top\.)?(\w{7}).*$/i, '$1$2') }]', ${ name }); + +top.onlocationchange = (event) => browser.runtime.sendMessage({ type: '$INIT$', options: { script: '${ script }' } }); + +;${ name };` + ) }, results => handle(results, LAST_ID = id, LAST_INSTANCE = instance, LAST_JS = script, LAST_TYPE = type)) + }) + }) + .then(() => running.push(id, instance)) + .catch(error => { throw error }); + break; + + // Soft reset (button reset) + case '_INIT_': + browser.tabs.executeScript(id, { code: LAST }, results => handle(results, LAST_ID, LAST_INSTANCE, LAST_JS, LAST_TYPE)); + break; + + // Hard reset (program reset) + case '$INIT$': + let t = type.toLowerCase(), + data = {}; + + browser.tabs.sendMessage(tab.id, { data, instance, [t]: script, instance_type: t, type: 'INITIALIZE' }); + // browser.tabs.getCurrent(tab => { + // instance = RandomName(); + // + // setTimeout(() => tabchange([ tab ]), 5000); + // }); + break; + + case 'FOUND': + FOUND[request.instance] = request.found; + break; + + case 'GRANT_PERMISSION': + await Save(`has/${ options[_type] }`, options.allowed); + await Save(`get/${ options[_type] }`, options.permissions); + break; + + case 'SEARCH_PLEX': + case 'VIEW_COUCHPOTATO': + case 'PUSH_COUCHPOTATO': + case 'PUSH_RADARR': + case 'PUSH_SONARR': + case 'PUSH_MEDUSA': + case 'PUSH_WATCHER': + case 'PUSH_OMBI': + case 'PUSH_SICKBEARD': + case 'OPEN_OPTIONS': + case 'SEARCH_FOR': + case 'SAVE_AS': + case 'DOWNLOAD_FILE': + /* Meant to be hnadled by background.js */ + return true; + break; + + default: + PLUGN_TERMINAL.warn(`Unable to find type "${ type }"`); + instance = RandomName(); + return false; + }; + } + + return true; +}); + +// this doesn't actually work... +// browser.tabs.onActiveChanged.addListener(tabchange); + +// workaround for the above +browser.tabs.onActivated.addListener(change => { + instance = RandomName(); + + browser.tabs.get(change.tabId, tab => tabchange([ tab ])); +}); + +let refresh; + +browser.tabs.onUpdated.addListener(refresh = (ID, change, tab) => { + instance = RandomName(); + + if(change.status == 'complete' && !tab.discarded) + tabchange([ tab ]); + else if(!tab.discarded) + setTimeout(() => refresh(ID, change, tab), 1000); +}); + +/* Attempt to suppress errors */ +if(browser.runtime.lastError) + browser.runtime.lastError.message; diff --git a/moz/plxdwnld.js b/moz/plxdwnld.js new file mode 100644 index 0000000..86fab87 --- /dev/null +++ b/moz/plxdwnld.js @@ -0,0 +1,146 @@ +/** plxdwnld - Pip Longrun / Ephellon +* +* This project is licensed under the terms of the MIT license, see https://piplongrun.github.io/plxdwnld/LICENSE.txt +* +* @author Pip Longrun +* @version 0.2 +* @see https://piplongrun.github.io/plxdwnld/ +* +*/ + +let plxdwnld = (() => { + let self = {}, R = RegExp, + baseURI, AccessToken, + RegExps = { + clientID: /server\/([a-f\d]{40})\//i, + metadataID: /key=%2Flibrary%2Fmetadata%2F(\d+)/i, + }, + URLExps = { + API_resource: 'https://plex.tv/api/resources?includeHttps=1&X-Plex-Token={token}', + API_library: '{baseuri}/library/metadata/{id}?X-Plex-Token={token}', + download: '{baseuri}{partkey}?download=1&X-Plex-Token={token}', + }, + access_token_path = '//Device[@clientIdentifier=\'{clientID}\']/@accessToken', + base_uri_path = '//Device[@clientIdentifier=\'{clientID}\']/Connection[@local=0]/@uri', + part_key_path = '//Media/Part[1]/@key'; + + // Errors + let ERROR = { + EMPTY: 'No response data was received', + NOT_PLEX: 'You are not browsing (or logged into) Plex', + NOT_MEDIA: 'You are not viewing a media item', + INVALID_TOKEN: 'Unable to find a valid Access Token', + }; + + let getXML = (url, callback) => { + fetch(`//cors-anywhere.herokuapp.com/${ url }`, { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest' } }) + .then(Q => Q.text()) + .then(text => { + if(!text.length) + throw ERROR.EMPTY; + + let Parser = new DOMParser(), + XML = Parser.parseFromString(text, 'text/xml'); + + callback(XML); + }) + .catch(error => { throw error }); + }; + + let getMetadata = (XML) => { + let clientID = RegExps.clientID.test(location.href)? + R.$1: + null; + + if(clientID) { + let access_token_node = XML.evaluate( + access_token_path.replace(/{clientid}/ig, clientID), + XML, + null, + XPathResult.FIRST_ORDERED_NODE_TYPE, + null + ), + base_uri_node = XML.evaluate( + base_uri_path.replace(/{clientid}/ig, clientID), + XML, + null, + XPathResult.FIRST_ORDERED_NODE_TYPE, + null + ); + + if(access_token_node.singleNodeValue && base_uri_node.singleNodeValue) { + AccessToken = access_token_node.singleNodeValue.textContent; + baseURI = base_uri_node.singleNodeValue.textContent; + + let metadataID = RegExps.metadataID.test(location.href)? + R.$1: + null; + + if(metadataID) + getXML( + URLExps.API_library + .replace(/{baseuri}/ig, baseURI) + .replace(/{id}/ig, metadataID) + .replace(/{token}/ig, AccessToken) + , GetDownloadURL + ); + else + throw ERROR.NOT_MEDIA; + } else { + throw ERROR.INVALID_TOKEN; + } + } else { + throw ERROR.NOT_MEDIA; + } + }; + + let GetDownloadURL = (XML) => { + let part_key_node = XML.evaluate(part_key_path, XML, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); + + if(part_key_node.singleNodeValue) { + let href = URLExps.download + .replace(/{baseuri}/ig, baseURI) + .replace(/{partkey}/ig, part_key_node.singleNodeValue.textContent) + .replace(/{token}/ig, AccessToken); + + top.postMessage({ href, tail: 'MP4', type: 'SEND_VIDEO_LINK', from: 'plex' }, '*'); + } else { + throw ERROR.NOT_MEDIA; + } + }; + + self.init = () => { + if(localStorage.myPlexAccessToken !== undefined) + getXML(URLExps.API_resource.replace(/{token}/ig, localStorage.myPlexAccessToken), getMetadata); + else + throw ERROR.NOT_PLEX; + }; + + return self; +})(); + +let NO_DEBUGGER = false; + +let terminal = + NO_DEBUGGER? + { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m }: + console; + +let check; + +check = document.body.onload = event => { + let loading = document.querySelector('.loading'); + + if(!loading) { + setTimeout(() => { + try { + plxdwnld.init(); + } catch(error) { + terminal.error('Failed to post message:', error); + setTimeout(check, 5000); + } + }, 5000) + } else { + setTimeout(check, 500); + } +}; diff --git a/moz/popup.css b/moz/popup.css new file mode 100644 index 0000000..82452b7 --- /dev/null +++ b/moz/popup.css @@ -0,0 +1,332 @@ +html, body { + height: 625px !important; + width: 625px !important; + + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.15) #3f4245; +} + +body { + background: url(noise.png), #3f4245 !important; + color: #333 !important; + font-family: Open Sans Regular, Helvetica Neue, Helvetica, Arial, sans-serif, system; + font-size: 1.25em !important; + flex-grow: 1 !important; + padding: 1px !important; + /* Disable for Firefox to work properly */ + /* overflow: hidden !important; */ + overflow-x: hidden; + /* position: absolute !important; */ +} + +a { + text-decoration: none !important; + cursor: pointer !important; + margin: 0 !important; +} + +img { + vertical-align: middle !important; + padding: 0 1em !important; +} + +label { + color: #eee !important; + cursor: inherit !important; + font-weight: 400 !important; + display: inline-block !important; + margin-bottom: 5px; + display: block !important; + position: relative !important; +/* + height: 165px !important; + width: 170px !important; +*/ +} + +table, footer { + height: 35% !important; + width: 100% !important; + font-size: 1em !important; + overflow: auto !important; + color: #fff !important; + font-weight: 400 !important; + padding-bottom: 1em !important; +} + +tr, footer > * { + box-shadow: none !important; + height: calc(33% - 25px); + width: calc(100% - 25px); + padding: 0 !important; +} + +td, footer > * > * { + padding: 15px !important; + line-height: 1.33 !important; + border-radius: 3px; + cursor: pointer !important; + text-transform: uppercase; + border: 0; + background: rgba(255, 255, 255, 0.3); + margin-bottom: 0; + vertical-align: middle; + white-space: nowrap; + user-select: none; + height: calc(100% - 30px); + width: calc(100% - 30px); + transition: box-shadow 0.5s !important; + display: table-cell !important; +} + +#movieo:hover { + box-shadow: 0 10px 128px inset #5DBCD4; +} + +#imdb:hover { + box-shadow: 0 10px 128px inset #E0AB00; +} + +#trakt:hover { + box-shadow: 0 10px 128px inset #ED2224; +} + +#letterboxd:hover { + box-shadow: 0 10px 128px inset #66CC33; +} + +#flenix:hover { + box-shadow: 0 10px 128px inset #EC164F; +} + +#tv-maze:hover { + box-shadow: 0 10px 128px inset #6EC4BA; +} + +#tvdb:hover { + box-shadow: 0 10px 128px inset #1C7E3E; +} + +#tmdb:hover { + box-shadow: 0 10px 128px inset #01D277; +} + +#vrv:hover { + box-shadow: 0 10px 128px inset #FFDD00; +} + +#hulu:hover { + box-shadow: 0 10px 128px inset #66AA33; +} + +#netflix:hover { + box-shadow: 0 10px 128px inset #E50914; +} + +#google:hover { + box-shadow: 0 10px 128px inset #EEEEEE; +} + +#itunes:hover { + box-shadow: 0 10px 128px inset #EEEEEE; +} + +#metacritic:hover { + box-shadow: 0 10px 128px inset #001B36; +} + +#fandango:hover { + box-shadow: 0 10px 128px inset #FF7300; +} + +#amazon:hover { + box-shadow: 0 10px 128px inset #FF9900; +} + +#vudu:hover { + box-shadow: 0 10px 128px inset #027FC5; +} + +#verizon:hover { + box-shadow: 0 10px 128px inset #E10000; +} + +#couch-potato:hover { + box-shadow: 0 10px 128px inset #ECB501; +} + +#rotten-tomatoes:hover { + box-shadow: 0 10px 128px inset #FA3008; +} + +#showrss:hover { + box-shadow: 0 10px 128px inset #6A8592; +} + +#vumoo:hover { + box-shadow: 0 10px 128px inset #DD1B2F; +} + +#shana-project:hover { + box-shadow: 0 10px 128px inset #FF0000; +} + +#youtube:hover { + box-shadow: 0 10px 128px inset #FF0000; +} + +#flickmetrix:hover { + box-shadow: 0 10px 128px inset #7A314E; +} + +#justwatch:hover { + box-shadow: 0 10px 128px inset #0E202C; +} + +#moviemeter:hover { + box-shadow: 0 10px 128px inset #000000; +} + +#allocine:hover { + box-shadow: 0 10px 128px inset #222222; +} + +#gostream:hover { + box-shadow: 0 10px 128px inset #028CC9; +} + +#tubi:hover { + box-shadow: 0 10px 128px inset #26262D; +} + +#webtoplex:hover { + box-shadow: 0 10px 128px inset #CC7B19; +} + +#local-plex:hover { + box-shadow: 0 10px 128px inset #F9BD03; +} + +#local-watcher:hover { + box-shadow: 0 10px 128px inset #EA554E; +} + +#local-radarr:hover { + box-shadow: 0 10px 128px inset #FFC230; +} + +#local-sonarr:hover { + box-shadow: 0 10px 128px inset #36C6F4; +} + +#local-couchpotato:hover { + box-shadow: 0 10px 128px inset #D20000; +} + +#local-ombi:hover { + box-shadow: 0 10px 128px inset #E48F34; +} + +#local-medusa:hover { + box-shadow: 0 10px 128px inset #26B043; +} + +#local-sickBeard:hover { + box-shadow: 0 10px 128px inset #296737; +} + +[save-file]:after, [cost-cash-low]:after, [cost-cash-med]:after, [cost-cash-hig]:after { + content: "____"; + color: transparent; + float: right; + width: 3em; + height: 3em; + margin-top: -8.5em; + margin-right: -1em; +} + +[pop-ups] label:after { + content: " \1F92C"; + float: right; +} + +[local] label:after { + content: " \1F5A5"; + float: right; +} + +[is-slow] label:after { + content: " \23f3"; + float: right; +} + +[is-shy] label:after, [is-dead] label:after { + content: " \1f910"; + float: right; +} + +[save-file]:after { + background: url("48.png") no-repeat center; +} + +[not-safe] label:after { + content: " \1F527"; + float: right; +} + +/* $1 - $10 */ +[cost-cash-low]:after { + background: url("$48.png") no-repeat center; +} + +/* $11 - $30 */ +[cost-cash-med]:after { + background: url("$$48.png") no-repeat center; +} + +/* $31+ */ +[cost-cash-hig]:after { + background: url("$$$48.png") no-repeat center; +} + +[disabled], [disabled]:hover { +/* box-shadow: 0 10px 128px inset #000 !important;*/ + opacity: 0.5 !important; +} + +[disabled] label:after { + content: " \23f3" !important; + float: right; +} + +*::-webkit-scrollbar { + width: 10px; +} + +*::-webkit-scrollbar-thumb { + min-height: 50px; + background: rgba(255, 255, 255, 0.15); + border: 2px solid rgba(0, 0, 0, 0); + border-radius: 8px; + background-clip: padding-box; +} + +*::-webkit-scrollbar-track { + background: url(noise.png) repeat, #3f4245 !important; +} + +*::-moz-scrollbar { + width: 10px; +} + +*::-moz-scrollbar-thumb { + min-height: 50px; + background: rgba(255, 255, 255, 0.15); + border: 2px solid rgba(0, 0, 0, 0); + border-radius: 8px; + background-clip: padding-box; +} + +*::-moz-scrollbar-track { + background: url(noise.png) repeat, #3f4245 !important; +} diff --git a/moz/popup.html b/moz/popup.html new file mode 100644 index 0000000..0ff1d98 --- /dev/null +++ b/moz/popup.html @@ -0,0 +1,250 @@ + + + + + + Web to Plex + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Netflix + + + + + Verizon + + + + + Trakt + + +
+ + Shana Project + + + + + YouTube + + + + + Rotten Tomatoes + + +
+ + Vumoo + + + + + fandango + + + + + Amazon + + +
+ + IMDb + + + + + CouchPotato + + + + + VRV + + +
+ + The MovieDb + + + + + Letterboxd + + + + + Hulu + + +
+ + The TVDb + + + + + Flickmetrix + + + + + JustWatch + + +
+ + iTunes + + + + + showRSS + + + + + Vudu + + +
+ + Movieo + + + + + TV Maze + + + + + Google + + +
+ + iTunes + + + + + Allocine + + + + + MovieMeter + + +
+ + GoStream + + + + + Tubi + + + + + Web to Plex + + +
+ + + + + + + diff --git a/moz/popup.js b/moz/popup.js new file mode 100644 index 0000000..d158ab6 --- /dev/null +++ b/moz/popup.js @@ -0,0 +1,92 @@ +function load(name) { + return JSON.parse(localStorage.getItem(btoa(name))); +} + +function save(name, data) { + return localStorage.setItem(btoa(name), JSON.stringify(data)); +} + +let table = document.body.querySelector('table'), + array = load('URLs'); + +if(array && array.length) { + let strings = [], + compiled = [], + object = {}, + width = 3; + + for(let count = 0, length = Math.ceil(array.length / width); count < length;) + for(let index = width * count++, name, url; index < count * width; index++) + object[name = array[index]] = (!/^(null|undefined)?$/.test( url = load(`${ name }.url`) || '' ))? + ` + + ${ name } + + + `: null; + + for(let index = 0, length = array.length, string; index < length; index++) + if(string = object[array[index]]) + compiled.push(string); + + for(let index = 0, length = compiled.length, string = ''; index <= length; index++) { + if((index > 0 && index % 3 == 0) || index >= length) + strings.push(string), + string = ''; + if(index < length) + string += compiled[index]; + } + + let html = ''; + + strings.map(string => + html += +` + ${ string } + ` + ); + + table.innerHTML = html + table.innerHTML; +} + +document.body.onload = function() { + let messages = { + "and": "{:{*}}", + "disabled": "Not yet implemented", + "is-shy": "Can only be accessed via: {*}", + "is-slow": "Resource intensive (loads slowly)", + "is-dead": "Isn't meant to show the Web to Plex button", + "local": "Opens a link to ^{*}", + "not-safe": "Updated irregularly, may drop support", + "pop-ups": "Contains annoying/intrusive ads and/or pop-ups", + "save-file": "Uses {*} before using your manager(s)", + // $0.99 one time; $0.99 - $9.99/mon + "cost-cash-low": "At least {*} (fair)", + // $9.99 one time; $9.99 - $29.99/mon + "cost-cash-med": "At least {*} (pricy)", + // $29.99 one time; $29.99 - $99.99/mon + "cost-cash-hig": "At least {*} (expensive)" + }, + parse = (string, attribute, element) => { + return string + .replace(/\{\$\}/g, element.title) + .replace(/\{\*\}/g, element.getAttribute(attribute)) + .replace(/\{\:([\w\- ]+)\}/g, ($0, $1, $$, $_) => + $1.split(' ').map($1 => parse(element.getAttribute($1), $1, element)) + ) + .replace(/\^([a-z])/gi, ($0, $1, $$, $_) => $1.toUpperCase()); + }, + selectors = []; + + for(let key in messages) + selectors.push(`[${ key }]`); + + let elements = document.querySelectorAll(selectors.join(',')); + + for(let element, index = 0, length = elements.length; index < length; index++) { + let number = 1; + for(let attribute in messages) + if(attribute in (element = elements[index]).attributes) + element.title += `\n${(number++)}) ${ parse(messages[attribute], attribute, element) }.`; + } +} diff --git a/moz/radarr.png b/moz/radarr.png new file mode 100644 index 0000000..f55ef6b Binary files /dev/null and b/moz/radarr.png differ diff --git a/moz/reload.16.png b/moz/reload.16.png new file mode 100644 index 0000000..f81b627 Binary files /dev/null and b/moz/reload.16.png differ diff --git a/moz/reload.48.png b/moz/reload.48.png new file mode 100644 index 0000000..5c61d2b Binary files /dev/null and b/moz/reload.48.png differ diff --git a/moz/rottentomatoes$.js b/moz/rottentomatoes$.js new file mode 100644 index 0000000..6fb798c --- /dev/null +++ b/moz/rottentomatoes$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'rottentomatoes' }))(); diff --git a/moz/rottentomatoes.js b/moz/rottentomatoes.js new file mode 100644 index 0000000..95075a3 --- /dev/null +++ b/moz/rottentomatoes.js @@ -0,0 +1,85 @@ +let script = { + "url": "*://*.rottentomatoes.com/([mt]|browse)/*", + + "ready": () => { + let element = $('#reviews').first; + + return !!element; + }, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title, type, year, image; + + type = script.getType(); + + switch(type) { + case 'movie': + case 'show': + title = $('.playButton + .title, [itemprop="name"], [class*="wrap__title" i]').first; + year = $('time').first; + image = $('[class*="posterimage" i]').first; + + if(!title) + return 1000; + + title = title.textContent.trim().replace(/(.+)\:[^]*$/, type == 'movie'? '$&': '$1'); + year = +year.textContent.replace(/[^]*(\d{4})/, '').trim(); + image = (image || {}).srcset; + + if(image) + image = image.replace(/([^\s]+)[^]*/, '$1'); + + return { type, title, year, image }; + break; + + case 'list': + let options, elements = $('.mb-movie'); + + elements.forEach((element, index, array) => { + let option = script.process(element); + + if(option) + options.push(option); + }); + + return options; + break; + + default: + return 1000; + break; + } + }, + + "getType": () => { + let { pathname } = top.location; + + return (/^\/browse\/i/.test(pathname))? + 'list': + (/^\/m/.test(pathname))? + 'movie': + (/^\/t/.test(pathname))? + 'show': + 'error'; + }, + + "process": (element) => { + let title = $('.movieTitle').first, + image = $('.poster').first, + type = $('[href^="/m/"], [href^="/t/"]').first; + + title = title.textContent.trim(); + image = image.src; + type = /\/([mt])\//i.test(type.href)? RegExp.$1 == 'm'? 'movie': 'show': null; + + if(!type) + return {}; + + if(type == 'show') + title = title.replace(/\s*\:\s*seasons?\s+\d+\s*/i, ''); + + return { type, title, image }; + }, +}; diff --git a/moz/rottentomatoes.png b/moz/rottentomatoes.png new file mode 100644 index 0000000..17b5f2b Binary files /dev/null and b/moz/rottentomatoes.png differ diff --git a/moz/settings.16.png b/moz/settings.16.png new file mode 100644 index 0000000..edba003 Binary files /dev/null and b/moz/settings.16.png differ diff --git a/moz/settings.48.png b/moz/settings.48.png new file mode 100644 index 0000000..082253b Binary files /dev/null and b/moz/settings.48.png differ diff --git a/moz/shanaproject.png b/moz/shanaproject.png new file mode 100644 index 0000000..d56fb84 Binary files /dev/null and b/moz/shanaproject.png differ diff --git a/moz/show.16.png b/moz/show.16.png new file mode 100644 index 0000000..47be280 Binary files /dev/null and b/moz/show.16.png differ diff --git a/moz/show.48.png b/moz/show.48.png new file mode 100644 index 0000000..353409a Binary files /dev/null and b/moz/show.48.png differ diff --git a/moz/showrss.png b/moz/showrss.png new file mode 100644 index 0000000..f317e88 Binary files /dev/null and b/moz/showrss.png differ diff --git a/moz/sonarr.png b/moz/sonarr.png new file mode 100644 index 0000000..93585db Binary files /dev/null and b/moz/sonarr.png differ diff --git a/moz/store-logo.png b/moz/store-logo.png new file mode 100644 index 0000000..11dee12 Binary files /dev/null and b/moz/store-logo.png differ diff --git a/moz/theme.css b/moz/theme.css new file mode 100644 index 0000000..6ad4146 --- /dev/null +++ b/moz/theme.css @@ -0,0 +1,38 @@ +/* Themes and other stylings for Web to Plex */ +/* Button Layout (Reference) + +// - an optional attribute value + +// The main button (container) +BUTTON .web-to-plex-button... + + // (Container) Holds the LIs that perform the various actions + UL + + // The Web to Plex (logo) button + LI #wtp-list-name.list-name + // The actionable anchor + A .list-action + IMG // Web to Plex logo + + // The Plex It! button + LI #wtp-plexit.list-item + IMG // Alarm bell + + // The hide button + LI #wtp-hide.list-item + IMG // Eye icon + + // The refresh button + LI #wtp-refresh.list-item + IMG // Refresh + + // The settings button + LI #wtp-options.list-item + IMG // Gear icon +*/ + +.web-to-plex-button.button-location-right { + left: unset !important; + right: 5px !important; +} diff --git a/moz/tmdb$.js b/moz/tmdb$.js new file mode 100644 index 0000000..6989aed --- /dev/null +++ b/moz/tmdb$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'tmdb' }))(); diff --git a/moz/tmdb.js b/moz/tmdb.js new file mode 100644 index 0000000..d4d7163 --- /dev/null +++ b/moz/tmdb.js @@ -0,0 +1,79 @@ +let script = { + "url": "*://*.themoviedb.org/(movie|tv)/\\d+", + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let type = script.getType(), + TMDbID = script.getTMDbID(), + title, year, image; + + let options; + + switch(type) { + case 'movie': + case 'tv': + title = $('.title > span > *:not(.release_date)').first; + year = $('.title .release_date').first; + image = $('img.poster').first; + + title = title.textContent.trim(); + year = +year.textContent.replace(/\(|\)/g, '').trim(); + image = (image || {}).src; + + if(type != 'movie') + type = 'show'; + + options = { type, title, year, image, TMDbID }; + break; + + case 'list': + let items = $('.item.card'); + + options = []; + + items.forEach(element => { + let option = script.process(element); + + if(option) + options.push(option); + }); + break; + + default: return null; + } + + return options; + }, + + "getType": () => { + let { pathname } = top.location; + + return (/\/(movie|tv)\/\d+/.test(pathname))? + RegExp.$1: + (/(^\/discover\/|\/(movie|tv)\/([^\d]+|\B))/i.test(pathname))? + 'list': + 'error'; + }, + + "getTMDbID": () => { + return +top.location.pathname.replace(/\/(?:movie|tv)\/(\d+).*/, '$1'); + }, + + "process": (element) => { + let title = $('.title').first, + year = $('.title + *').first, + image = $('.poster').first, + type = title.id.split('_'), + TMDbID = +type[1]; + + title = title.textContent.trim(); + year = year.textContent; + image = image.src; + type = (type[0] == 'movie'? 'movie': 'show'); + + year = +year; + + return { type, title, year, image, TMDbID }; + }, +}; diff --git a/moz/tmdb.png b/moz/tmdb.png new file mode 100644 index 0000000..b2a0165 Binary files /dev/null and b/moz/tmdb.png differ diff --git a/moz/trakt$.js b/moz/trakt$.js new file mode 100644 index 0000000..4e035f8 --- /dev/null +++ b/moz/trakt$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'trakt' }))(); diff --git a/moz/trakt.js b/moz/trakt.js new file mode 100644 index 0000000..77c531d --- /dev/null +++ b/moz/trakt.js @@ -0,0 +1,104 @@ +/** TODO + - re-enable list functionality (fix it) +**/ + +let script = { + "url": "*://*.trakt.tv/(movie|show)s/*", + + "ready": () => !$('#info-wrapper ul.external, .format-date').empty, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let type = script.getType(), + IMDbID, TMDbID, TVDbID, + title, year, image, options; + + switch(type) { + case 'movie': + case 'show': + title = $('.mobile-title').first; + year = $('.mobile-title .year').first; + image = $('.poster img.real[alt="poster"i]').first; + IMDbID = script.getIMDbID(); + TMDbID = script.getTMDbID(); + TVDbID = script.getTVDbID(); + + if(!IMDbID && !TMDbID && !TVDbID) + return 5000; + + title = title.textContent.replace(/(.+)(\d{4}).*?$/, '$1').replace(/\s*\:\s*Season.*$/i, '').trim(); + year = +(R.$2 || year.textContent).trim(); + image = (image || {}).src; + + options = { type, title, year, image, IMDbID, TMDbID, TVDbID }; + break; + + case 'list': + let items = $('*'); + + options = []; + + items.forEach((element, index, array) => { + let option = script.process(element, items); + + if(option) + options.push(option); + }); + break; + + default: + return null; + } + + return options; + }, + + "getType": () => { + let { pathname } = top.location; + + return ( + // /^\/(dashboard|calendars|people|search|(?:movie|show)s?\/(?:trending|popular|watched|collected|anticipated|boxoffice)|$)/i.test(pathname)? + // 'list': + /^\/(movie|show)s\//i.test(pathname)? + RegExp.$1: + 'error' + ) + }, + + "getIMDbID": () => { + let link = $( + // HTTPS and HTTP + '[href*="imdb.com/title/tt"]' + ).first; + + if(link) + return link.href.replace(/^.*?imdb\.com\/.+\b(tt\d+)\b/, '$1'); + }, + + "getTMDbID": () => { + let link = $( + // HTTPS and HTTP + '[href*="themoviedb.org/"]' + ).first; + + if(link) + return link.href.replace(/^.*?themoviedb.org\/(?:movie|tv|shows?|series)\/(\d+).*?$/, '$1'); + }, + + "getTVDbID": () => { + let link = $( + // HTTPS and HTTP + '[href*="thetvdb.com/"]' + ).first; + + if(link) + return link.href.replace(/^.*?thetvdb.com\/.+\/(\d+)\b.*?$/, '$1'); + }, + + "process": (element, elements) => { + let type, title, year; + + return { type, title, year }; + }, +}; diff --git a/moz/trakt.png b/moz/trakt.png new file mode 100644 index 0000000..cfc4316 Binary files /dev/null and b/moz/trakt.png differ diff --git a/moz/tubi$.js b/moz/tubi$.js new file mode 100644 index 0000000..fbe9b35 --- /dev/null +++ b/moz/tubi$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'tubi' }))(); diff --git a/moz/tubi.js b/moz/tubi.js new file mode 100644 index 0000000..7ede144 --- /dev/null +++ b/moz/tubi.js @@ -0,0 +1,22 @@ +let script = { + "url": "*://*.tubitv.com/(movies|series)/\\d+/*", + + "timeout": 1000, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('._1mbQP').first, + year = $('._3BhXb').first, + image = $('._2TykB').first, + type = script.getType(); // described below + + title = title.textContent.trim(); + year = +year.textContent.replace(/[^]*\((\d+)\)[^]*/g, '$1').trim(); + image = image.getAttribute('style').replace(/[^]+url\('([^]+?)'\)/, '$1'); + + return { type, title, year, image }; + }, + + "getType": () => (/^\/movies?/.test(top.location.pathname)? 'movie': 'show'), +}; diff --git a/moz/tubi.png b/moz/tubi.png new file mode 100644 index 0000000..5a8ce56 Binary files /dev/null and b/moz/tubi.png differ diff --git a/moz/tv-maze.png b/moz/tv-maze.png new file mode 100644 index 0000000..23858e1 Binary files /dev/null and b/moz/tv-maze.png differ diff --git a/moz/tv-show.poster.jpg b/moz/tv-show.poster.jpg new file mode 100644 index 0000000..cc96009 Binary files /dev/null and b/moz/tv-show.poster.jpg differ diff --git a/moz/tvdb$.js b/moz/tvdb$.js new file mode 100644 index 0000000..8486015 --- /dev/null +++ b/moz/tvdb$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'tvdb' }))(); diff --git a/moz/tvdb.js b/moz/tvdb.js new file mode 100644 index 0000000..ba29377 --- /dev/null +++ b/moz/tvdb.js @@ -0,0 +1,44 @@ +let script = { + "url": "*://*.thetvdb.com/series/*", + + "ready": () => !$('#series_basic_info').empty, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('#series_title, .translated_title').first, + image = $('img[src*="/posters/"]').first, + type = 'show', + TVDbID = script.getTVDbID(), + Db = {}, year; + + title = title.textContent.trim(); + image = (image || {}).src; + + $('#series_basic_info').first.textContent + .replace(/^\s+|\s+$/g, '') + .replace(/^\s+$/gm, '') + .replace(/^\s+(\S)/gm, '$1') + .split(RegExp(`\\n*\\n*`)) + .forEach(value => { + value = value.split(/\n+/, 2); + + let n = value[0], v = value[1]; + + n = n.replace(/^([\w\s]+).*$/, '$1').replace(/\s+/g, '_').toLowerCase(); + + Db[n] = /,/.test(v)? v.split(/\s*,\s*/): v; + }); + + year = +(((Db.first_aired || YEAR) + '').slice(0, 4)); + + return { type, title, year, image, TVDbID }; + }, + + "getTVDbID": () => { + let { pathname } = top.location; + + if(/\/series\/(\d+)/.test(pathname)) + return RegExp.$1; + }, +}; diff --git a/moz/tvdb.png b/moz/tvdb.png new file mode 100644 index 0000000..2dd3cf9 Binary files /dev/null and b/moz/tvdb.png differ diff --git a/moz/tvmaze$.js b/moz/tvmaze$.js new file mode 100644 index 0000000..d704d5c --- /dev/null +++ b/moz/tvmaze$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'tvmaze' }))(); diff --git a/moz/tvmaze.js b/moz/tvmaze.js new file mode 100644 index 0000000..3332237 --- /dev/null +++ b/moz/tvmaze.js @@ -0,0 +1,27 @@ +let script = { + "url": "*://*.tvmaze.com/shows/*", + + "ready": () => !$('#general-info-panel .rateit').empty, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('header.columns > h1').first, + year = $('#year').first, + image = $('figure img').first, + type = 'show', + TVDbID = script.getTVDbID(); + + title = title.textContent.trim(); + year = +year.textContent.replace(/\((\d+).+\)/, '$1'); + image = (image || {}).src; + + return { type, title, year, image, TVDbID }; + }, + + "getTVDbID": () => { + let { pathname } = top.location; + + return pathname.replace(/\/shows\/(\d+).*/, '$1'); + }, +}; diff --git a/moz/utils.js b/moz/utils.js new file mode 100644 index 0000000..a753e4d --- /dev/null +++ b/moz/utils.js @@ -0,0 +1,2835 @@ +/* eslint-disable no-unused-vars */ +/* global configuration, init, Update, "Helpers" */ + +let configuration, init, Update; + +(async date => { + + // default date items + let YEAR = date.getFullYear(), + MONTH = date.getMonth() + 1, + DATE = date.getDate(), + // Notification items + NOTIFIED = false, + RUNNING = false, + // Other items + /* Items that the user has already asked for */ + CAUGHT; + + // simple helpers + let extURL = url => browser.runtime.getURL(url), + $ = (selector, container) => queryBy(selector, container), + // DO NOT EXPOSE + __CONFIG__, ALLOWED, PERMISS; + + let IMG_URL = { + 'nil': extURL('null.png'), + 'icon_16': extURL('16.png'), + 'icon_48': extURL('48.png'), + 'hide_icon_16': extURL('hide.16.png'), + 'hide_icon_48': extURL('hide.48.png'), + 'show_icon_16': extURL('show.16.png'), + 'show_icon_48': extURL('show.48.png'), + 'close_icon_16': extURL('close.16.png'), + 'close_icon_48': extURL('close.48.png'), + 'icon_white_16': extURL('_16.png'), + 'icon_white_48': extURL('_48.png'), + 'plexit_icon_16': extURL('plexit.16.png'), + 'plexit_icon_48': extURL('plexit.48.png'), + 'reload_icon_16': extURL('reload.16.png'), + 'reload_icon_48': extURL('reload.48.png'), + 'icon_outline_16': extURL('o16.png'), + 'icon_outline_48': extURL('o48.png'), + 'noise_background': extURL('noise.png'), + 'settings_icon_16': extURL('settings.16.png'), + 'settings_icon_48': extURL('settings.48.png'), + }; + + // the storage - priority to sync + const UTILS_STORAGE = browser.storage.sync || browser.storage.local; + + async function load(name = '') { + if(!name) + return /* invalid name */; + + name = '~/cache/' + (name.toLowerCase().replace(/\s+/g, '_')); + + return new Promise((resolve, reject) => { + function LOAD(DISK) { + let data = JSON.parse(DISK[name] || null); + + return resolve(data); + } + + UTILS_STORAGE.get(null, DISK => { + if(browser.runtime.lastError) + browser.runtime.lastError.message || + browser.storage.local.get(null, LOAD); + else + LOAD(DISK); + }); + }); + } + + async function save(name = '', data) { + if(!name) + return /* invalid name */; + + name = '~/cache/' + (name.toLowerCase().replace(/\s+/g, '_')); + data = JSON.stringify(data); + + // erase entries after 400-500 have been made + UTILS_STORAGE.get(null, items => { + let array = [], bytes = 0; + + for(let item in items) { + let object = items[item]; + + array.push(item); + bytes += (typeof object == 'string'? object.length * 8: typeof object == 'boolean'? 8: JSON.stringify(object).length * 8)|0; + } + + if((UTILS_STORAGE.MAX_ITEMS && array.length >= UTILS_STORAGE.MAX_ITEMS) || bytes >= UTILS_STORAGE.QUOTA_BYTES) { + UTILS_TERMINAL.warn('Exceeded quota. Erasing cache...'); + + for(let item in items) + if(/^~\/cache\/(?!get|has)/i.test(item)) + UTILS_STORAGE.remove(item); + + UTILS_TERMINAL.log('Cache erased'); + } + }); + + await UTILS_STORAGE.set({[name]: data}, () => data); + + return name; + } + + async function remove(name) { + if(!name) + return /* invalid name */; + + return await UTILS_STORAGE.remove(['~/cache/' + (name.toLowerCase().replace(/\s+/g, '_'))]); + } + + function encode(data) { + if(/^[\u0000-\u00ff]+$/.test(data)) + return btoa(data); + else + return data; + } + + function decode(data) { + if(/^[a-z\d\+\/\=]+$/i.test(data)) + return atob(data); + else + return data; + } + + /* Notifications */ + // create and/or queue a notification + // state = "warning" - red + // state = "error" + // state = "update" - blue + // state = "info" - grey + // anything else for state will show as orange + class Notification { + constructor(state, text, timeout = 7000, callback = () => {}, requiresClick = true) { + let queue = (Notification.queue = Notification.queue || { list: [] }), + last = queue.list[queue.list.length - 1] || document.queryBy('.web-to-plex-notification').first; + + if(!__CONFIG__) { + Options(); + + throw 'No configuration saved...'; + } + + if(((state == 'error' || state == 'warning') && __CONFIG__.NotifyNewOnly && /\balready\s+(exists?|(been\s+)?added)\b/.test(text)) || (__CONFIG__.NotifyOnlyOnce && NOTIFIED && state === 'info')) + return /* Don't match /.../i as to not match item titles */; + NOTIFIED = true; + + if(last && !last.done) + return (last => setTimeout(() => new Notification(state, text, timeout, callback, requiresClick), +(new Date) - last.start))(last); + + let element = furnish(`div.web-to-plex-notification.${state}`, { + onmouseup: event => { + let notification = Notification.queue[event.target.id], + element = notification.element; + + notification.done = true; + Notification.queue.list.splice(notification.index, 1); + clearTimeout(notification.job); + element.remove(); + + let removed = delete Notification.queue[notification.id]; + + return (event.requiresClick)? null: notification.callback(removed); + } + }, text); + + queue[element.id = +(new Date)] = { + start: +element.id, + stop: +element.id + timeout, + span: +timeout, + done: false, + index: queue.list.length, + job: setTimeout(() => element.onmouseup({ target: element, requiresClick }), timeout), + id: +element.id, + callback, element + }; + queue.list.push(queue[element.id]); + + document.body.appendChild(element); + + return queue[element.id]; + } + } + + class Prompt { + constructor(prompt_type, options, callback = () => {}, container = document.body) { + let prompt, remove, + array = (options instanceof Array? options: [].slice.call(options)), + data = [...array], + profiles = { + movie: JSON.parse( + __CONFIG__.usingRadarr? + __CONFIG__.radarrQualities: + __CONFIG__.usingWatcher? + __CONFIG__.watcherQualities: + '[]' + ), + show: JSON.parse( + __CONFIG__.usingSonarr? + __CONFIG__.sonarrQualities: + __CONFIG__.usingMedusa? + __CONFIG__.medusaQualities: + __CONFIG__.usingSickBeard? + __CONFIG__.sickBeardQualities: + '[]' + ) + }, + locations = { + movie: JSON.parse( + __CONFIG__.usingRadarr? + __CONFIG__.radarrStoragePaths: + __CONFIG__.usingWatcher? + __CONFIG__.watcherStoragePaths: + '[]' + ), + show: JSON.parse( + __CONFIG__.usingSonarr? + __CONFIG__.sonarrStoragePaths: + __CONFIG__.usingMedusa? + __CONFIG__.medusaStoragePaths: + __CONFIG__.usingSickBeard? + __CONFIG__.sickBeardStoragePaths: + '[]' + ) + }, + defaults = { + movie: ( + __CONFIG__.usingRadarr? + { quality: __CONFIG__.__radarrQuality, location: __CONFIG__.__radarrStoragePath }: + {} + ), + show: ( + __CONFIG__.usingSonarr? + { quality: __CONFIG__.__sonarrQuality, location: __CONFIG__.__sonarrStoragePath }: + __CONFIG__.usingMedusa? + { quality: __CONFIG__.__medusaQuality, location: __CONFIG__.__medusaStoragePath }: + __CONFIG__.usingSickBeard? + { quality: __CONFIG__.__sickBeardQuality, location: __CONFIG__.__sickBeardStoragePath }: + {} + ) + }; + + let preX = document.queryBy('.web-to-plex-prompt').first; + + if(preX) + return /* Ignore while another prompt is open, prevents double prompts */; + + switch(prompt_type) { + /* Allows the user to add and remove items from a list */ + case 'prompt': + case 'input': + remove = element => { + let prompter = $('.web-to-plex-prompt').first, + header = $('.web-to-plex-prompt-header').first, + counter = $('.web-to-plex-prompt-options').first; + + if(element === true) + return prompter.remove(); + else + element.remove(); + + data.splice(+element.value, 1, null); + header.innerText = 'Approve ' + counter.children.length + (counter.children.length == 1?' item': ' items'); + }; + + prompt = furnish('div.web-to-plex-prompt', {}, + furnish('div.web-to-plex-prompt-body', {}, + // The prompt's title + furnish('h1.web-to-plex-prompt-header', {}, 'Approve ' + array.length + (array.length == 1? ' item': ' items')), + + // The prompt's items + furnish('div.web-to-plex-prompt-options', {}, + ...(ITEMS => { + let elements = []; + + for(let index = 0, length = ITEMS.length, ITEM, P_QUA, P_LOC; index < length; index++) { + ITEM = ITEMS[index]; + + elements.push( + furnish('li.web-to-plex-prompt-option.mutable', { value: index, innerHTML: `

${ index + 1 } \u00b7 ${ ITEM.title }${ ITEM.year? ` (${ ITEM.year })`: '' } \u2014 ${ ITEM.type }

` }, + furnish('button.remove', { title: `Remove "${ ITEM.title }"`, onmouseup: event => { remove(event.target.parentElement); event.target.remove() } }), + ( + __CONFIG__.PromptQuality? + P_QUA = furnish('select.quality', { index, onchange: event => data[event.target.getAttribute('index')].quality = event.target.value }, ...profiles[/(movie|film|cinema)/i.test(ITEM.type)?'movie':'show'].map(Q => furnish('option', { value: Q.id }, Q.name))): + '' + ),( + __CONFIG__.PromptLocation? + P_LOC = furnish('select.location', { index, onchange: event => data[event.target.getAttribute('index')].location = event.target.value }, ...locations[/(movie|film|cinema)/i.test(ITEM.type)?'movie':'show'].map(Q => furnish('option', { value: Q.id }, Q.path))): + '' + ) + ) + ); + + if(P_QUA) P_QUA.value = defaults[ITEM.type].quality; + if(P_LOC) P_LOC.value = defaults[ITEM.type].location; + + P_QUA = P_LOC = null; + } + + return elements + })(array) + ), + + // The engagers + furnish('div.web-to-plex-prompt-footer', {}, + furnish('input.web-to-plex-prompt-input[type=text]', { placeholder: 'Add an item (enter to add): Title (Year) Type / ID Type', title: 'Solo: A Star Wars Story (2018) movie / tt3778644 m', onkeydown: async event => { + if(event.keyCode == 13) { + let title, year, type, self = event.target, R = RegExp, + movie = /^(m(?:ovies?)?|f(?:ilms?)?|c(?:inemas?)?)/i, + Db, IMDbID, TMDbID, TVDbID, value = self.value; + + self.setAttribute('disabled', self.disabled = true); + self.value = `Searching for "${ value }"...`; + data = data.filter(value => value !== null && value !== undefined); + + if(/^\s*((?:tt)?\d+)(?:\s+(\w+)|\s*)?$/i.test(value)) { + let APIID = R.$1, + type = R.$2 || (data.length? data[0].type: 'movie'), + APIType = movie.test(type)? /^tt/i.test(APIID)? 'imdb': 'tmdb': 'tvdb'; + + type = movie.test(type)? 'movie': 'show'; + + Db = await Identify({ type, APIID, APIType }); + IMDbID = Db.imdb; + TMDbID = Db.tmdb; + TVDbID = Db.tvdb; + + title = Db.title; + year = Db.year; + } else if(/^([^]+)(\s*\(\d{2,4}\)\s*|\s+\d{2,4}\s+)([\w\s\-]+)$/.test(value)) { + title = R.$1; + year = R.$2 || YEAR + ''; + type = R.$3 || (data.length? data[0].type: 'movie'); + + year = +year.replace(/\D/g, '').replace(/^\d{2}$/, '20$&'); + type = movie.test(type)? 'movie': 'show'; + + Db = await Identify({ type, title, year }); + IMDbID = Db.imdb; + TMDbID = Db.tmdb; + TVDbID = Db.tvdb; + } + + event.preventDefault(); + if(type && title && !(/^(?:tt)?$/i.test(IMDbID || '') && /^0?$/.test(+TMDbID | 0) && /^0?$/.test(+TVDbID | 0))) { + remove(true); + new Prompt(prompt_type, [{ ...Db, type, IMDbID, TMDbID, TVDbID }, ...data], callback, container); + } else { + self.disabled = self.removeAttribute('disabled'); + self.value = value; + new Notification('error', `Couldn't find "${ value }"`); + } + } + } }), + furnish('button.web-to-plex-prompt-decline', { onmouseup: event => { remove(true); callback([]) }, title: 'Close' }, '\u2718'), + furnish('button.web-to-plex-prompt-accept', { onmouseup: event => { remove(true); new Prompt(prompt_type, options, callback, container) }, title: 'Reset' }, '\u21BA'), + furnish('button.web-to-plex-prompt-accept', { onmouseup: event => { remove(true); callback(data.filter(value => value !== null && value !== undefined)) }, title: 'Continue' }, '\u2714') + ) + ) + ); + break; + + /* Allows the user to remove predetermined items */ + case 'select': + remove = element => { + let prompter = $('.web-to-plex-prompt').first, + header = $('.web-to-plex-prompt-header').first, + counter = $('.web-to-plex-prompt-options').first; + + if(element === true) + return prompter.remove(); + else + element.remove(); + + data.splice(+element.value, 1, null); + header.innerText = 'Approve ' + counter.children.length + (counter.children.length == 1?' item': ' items'); + }; + + prompt = furnish('div.web-to-plex-prompt', {}, + furnish('div.web-to-plex-prompt-body', {}, + // The prompt's title + furnish('h1.web-to-plex-prompt-header', {}, 'Approve ' + array.length + (array.length == 1? ' item': ' items')), + + // The prompt's items + furnish('div.web-to-plex-prompt-options', {}, + ...(ITEMS => { + let elements = []; + + for(let index = 0, length = ITEMS.length, ITEM, P_QUA, P_LOC; index < length; index++) { + ITEM = ITEMS[index]; + + elements.push( + furnish('li.web-to-plex-prompt-option.mutable', { value: index, innerHTML: `

${ index + 1 } \u00b7 ${ ITEM.title }${ ITEM.year? ` (${ ITEM.year })`: '' } \u2014 ${ ITEM.type }

` }, + furnish('button.remove', { title: `Remove "${ ITEM.title }"`, onmouseup: event => { remove(event.target.parentElement); event.target.remove() } }), + ( + __CONFIG__.PromptQuality? + P_QUA = furnish('select.quality', { index, onchange: event => data[event.target.getAttribute('index')].quality = event.target.value }, ...profiles[/(movie|film|cinema)/i.test(ITEM.type)?'movie':'show'].map(Q => furnish('option', { value: Q.id }, Q.name))): + '' + ),( + __CONFIG__.PromptLocation? + P_LOC = furnish('select.location', { index, onchange: event => data[event.target.getAttribute('index')].location = event.target.value }, ...locations[/(movie|film|cinema)/i.test(ITEM.type)?'movie':'show'].map(Q => furnish('option', { value: Q.id }, Q.path))): + '' + ) + ) + ); + + if(P_QUA) P_QUA.value = defaults[ITEM.type].quality; + if(P_LOC) P_LOC.value = defaults[ITEM.type].location; + + P_QUA = P_LOC = null; + } + + return elements + })(array) + ), + + // The engagers + furnish('div.web-to-plex-prompt-footer', {}, + furnish('button.web-to-plex-prompt-decline', { onmouseup: event => { remove(true); callback([]) }, title: 'Close' }, '\u2718'), + furnish('button.web-to-plex-prompt-accept', { onmouseup: event => { remove(true); new Prompt(prompt_type, options, callback, container) }, title: 'Reset' }, '\u21BA'), + furnish('button.web-to-plex-prompt-accept', { onmouseup: event => { remove(true); callback(data.filter(value => value !== null && value !== undefined)) }, title: 'Continue' }, '\u2714') + ) + ) + ); + break; + + /* Allows the user to modify a single item (before being pushed) */ + case 'modify': + let { title, year, type, IMDbID, TMDbID, TVDbID } = options, + P_QUA, P_LOC; + + let i = IMDbID, + t = TMDbID, + v = TVDbID, + s = 'style="text-decoration: none !important; color: #cc7b19 !important; font-style: italic !important;" target="_blank"'; + + i = /^tt-?$/.test(i)? '': i; + t = /^0?$/.test(t)? '': t; + v = /^0?$/.test(v)? '': v; + + remove = element => { + let prompter = $('.web-to-plex-prompt').first, + header = $('.web-to-plex-prompt-header').first, + counter = $('.web-to-plex-prompt-options').first; + + if(element === true) + return prompter.remove(); + else + element.remove(); + }; + + type = /(movie|film|cinema)/i.test(type)?'movie':'show'; + + prompt = furnish('div.web-to-plex-prompt', {}, + furnish('div.web-to-plex-prompt-body', {}, + // The prompt's title + furnish('h1.web-to-plex-prompt-header', { innerHTML: `${ title }${ year? ` (${ year })`: '' } \u2014 ${ type }` }), + + // The prompt's items + furnish('div.web-to-plex-prompt-options', {}, + furnish('div.web-to-plex-prompt-option', { innerHTML: `${ i? `${i}`: '/' } \u2014 ${ t? `${t}`: '/' } \u2014 ${ v? `${v}`: '/' }` }), + ( + __CONFIG__.PromptQuality? + P_QUA = furnish('select.quality', { onchange: event => options.quality = event.target.value }, ...profiles[type].map(Q => furnish('option', { value: Q.id }, Q.name))): + '' + ), + furnish('br'), + ( + __CONFIG__.PromptLocation? + P_LOC = furnish('select.location', { onchange: event => options.location = event.target.value }, ...locations[type].map(Q => furnish('option', { value: Q.id }, Q.path))): + '' + ) + ), + + // The engagers + furnish('div.web-to-plex-prompt-footer', {}, + furnish('button.web-to-plex-prompt-decline', { onmouseup: event => { remove(true); callback([]) }, title: 'Close' }, '\u2718'), + furnish('button.web-to-plex-prompt-accept', { onmouseup: event => { remove(true); new Prompt(prompt_type, options, callback, container) }, title: 'Reset' }, '\u21BA'), + furnish('button.web-to-plex-prompt-accept', { onmouseup: event => { remove(true); callback(options) }, title: 'Continue' }, '\u2714') + ) + ) + ); + + if(P_QUA) P_QUA.value = defaults[type].quality; + if(P_LOC) P_LOC.value = defaults[type].location; + + P_QUA = P_LOC = null; + break; + + case 'permission': + let { permission, name, alias } = options; + let existing, permissions; + + /* Only one permission prompt allowed */ + if(!(existing = $('.web-to-plex-prompt[type=permission]')).empty) + return existing.first; + + UTILS_TERMINAL.log(`Asking for permission(s):`, options); + + remove = element => { + let prompter = $('.web-to-plex-prompt').first, + header = $('.web-to-plex-prompt-header').first, + counter = $('.web-to-plex-prompt-options').first; + + if(element === true) + return prompter.remove(); + else + element.remove(); + }; + + callback = (allowed, permissions) => { + save(`has/${ name }`, allowed); + save(`get/${ name }`, permissions); + + ALLOWED = allowed; + PERMISS = permissions; + + ParsedOptions(); + + return Update(`GRANT_PERMISSION`, { allowed, permissions }, true), + (init && !RUNNING? (init(), RUNNING = true): RUNNING = false); + }; + + prompt = furnish('div.web-to-plex-prompt', {}, + furnish('div.web-to-plex-prompt-body', {}, + // The prompt's title + furnish('h1.web-to-plex-prompt-header', { innerHTML: `${ alias || name } (${ location.host }) would like:` }), + + // The prompt's items + furnish('div.web-to-plex-prompt-options', {}, + ...((permissions = permission.split(/\s*,\s*/).filter(v=>v&&v.length)).map( + __permission => + furnish('div.web-to-plex-prompt-option', { innerHTML: `Access to your ${ __permission } information` }) + ) + ) + ), + + + // The engagers + furnish('div.web-to-plex-prompt-footer', {}, + furnish('button.web-to-plex-prompt-decline', { onmouseup: event => { remove(true); callback(false, {}) }, title: 'Deny' }, '\u2718'), + furnish('button.web-to-plex-prompt-accept', { onmouseup: event => { remove(true); callback(true, permissions) }, title: 'Allow' }, '\u2714') + ) + ) + ); + break; + + default: + return UTILS_TERMINAL.warn(`Unknown prompt type "${ prompt_type }"`); + break; + } + + prompt.setAttribute('type', prompt_type); + + return container.append(prompt), prompt; + } + } + + // open up the options page + function Options() { + browser.runtime.sendMessage({ + type: 'OPEN_OPTIONS' + }); + } + + // "secret frame" + function sFrame(url, callbacks) { + let { success, error } = callbacks; + + let frame = document.furnish('iframe#web-to-plex-sframe', { + src: url, + style: ` + display: none !important; + opacity: 0 !important; + visibility: hidden !important; + `, + + onload: success, + onerror: error, + }); + + // todo: make iframe, load, delete + document.body.append(frame); + } + + // Send an update query to background.js + Update = (type, options = {}, postToo) => { + if(configuration) + UTILS_TERMINAL.log(`Requesting update [post-to-top: ${ !!postToo }]: ${ type }`, options); + else + return sFrame(extURL(`options.html#save`), { + success: async event => { + let self = event.target; + + await ParsedOptions(); + + Update(type, options, postToo); + + self.remove(); + }, + + error: async event => { + let self = event.target; + self.remove(); + + new Notification('error', `Fill in missing Web to Plex options`, 15000, Options, false); + + throw `Unable to set configuration variable: ${ JSON.stringify(configuration) }`; + } + }); + + browser.runtime.sendMessage({ + type, + options + }); + + if(postToo) + top.postMessage(options); + }; + + // get the saved options + function options() { + return new Promise((resolve, reject) => { + function handleOptions(options) { + if((!options.plexToken || !options.servers) && !options.IGNORE_PLEX) + return reject(new Error('Required options are missing')), + null; + + let server, o; + + if(!options.IGNORE_PLEX) { + // For now we support only one Plex server, but the options already + // allow multiple for easy migration in the future. + server = options.servers[0]; + o = { + server: { + ...server, + // Compatibility for users who have not updated their settings yet. + connections: server.connections || [{ uri: server.url }] + }, + ...options + }; + + options.plexURL = o.plexURL? + `${ o.plexURL }web#!/server/${ o.server.id }/`: + `https://app.plex.tv/web/app#!/server/${ o.server.id }/`; + } else { + o = options; + } + + if(o.couchpotatoBasicAuthUsername) + o.couchpotatoBasicAuth = { + username: o.couchpotatoBasicAuthUsername, + password: o.couchpotatoBasicAuthPassword + }; + + // TODO: stupid copy/pasta + if(o.watcherBasicAuthUsername) + o.watcherBasicAuth = { + username: o.watcherBasicAuthUsername, + password: o.watcherBasicAuthPassword + }; + + if(o.radarrBasicAuthUsername) + o.radarrBasicAuth = { + username: o.radarrBasicAuthUsername, + password: o.radarrBasicAuthPassword + }; + + if(o.sonarrBasicAuthUsername) + o.sonarrBasicAuth = { + username: o.sonarrBasicAuthUsername, + password: o.sonarrBasicAuthPassword + }; + + if(o.medusaBasicAuthUsername) + o.medusaBasicAuth = { + username: o.medusaBasicAuthUsername, + password: o.medusaBasicAuthPassword + }; + + if(o.sickBeardBasicAuthUsername) + o.sickBeardBasicAuth = { + username: o.sickBeardBasicAuthUsername, + password: o.sickBeardBasicAuthPassword + }; + + if(o.usingOmbi && o.ombiURLRoot && o.ombiToken) { + o.ombiURL = o.ombiURLRoot; + } else { + delete o.ombiURL; // prevent variable ghosting + } + + if(o.usingCouchPotato && o.couchpotatoURLRoot && o.couchpotatoToken) { + o.couchpotatoURL = `${ items.couchpotatoURLRoot }/api/${encodeURIComponent(o.couchpotatoToken)}`; + } else { + delete o.couchpotatoURL; // prevent variable ghosting + } + + if(o.usingWatcher && o.watcherURLRoot && o.watcherToken) { + o.watcherURL = o.watcherURLRoot; + } else { + delete o.watcherURL; // prevent variable ghosting + } + + if(o.usingRadarr && o.radarrURLRoot && o.radarrToken) { + o.radarrURL = o.radarrURLRoot; + } else { + delete o.radarrURL; // prevent variable ghosting + } + + if(o.usingSonarr && o.sonarrURLRoot && o.sonarrToken) { + o.sonarrURL = o.sonarrURLRoot; + } else { + delete o.sonarrURL; // prevent variable ghosting + } + + if(o.usingMedusa && o.medusaURLRoot && o.medusaToken) { + o.medusaURL = o.medusaURLRoot; + } else { + delete o.medusaURL; // prevent variable ghosting + } + + if(o.usingSickBeard && o.sickBeardURLRoot && o.sickBeardToken) { + o.sickBeardURL = o.sickBeardURLRoot; + } else { + delete o.sickBeardURL; // prevent variable ghosting + } + + resolve(o); + } + + UTILS_STORAGE.get(null, options => { + if(browser.runtime.lastError) + browser.runtime.lastError.message || + browser.storage.local.get(null, handleOptions); + else + handleOptions(options); + }); + }); + } + + // self explanatory, returns an object; sets the configuration variable + function ParsedOptions() { + return options() + .then( + options => { + configuration = {}; + + /* Don't expose the user's authentication information to sites */ + for(let key in options) + if(/username|password|token|api|server|url|storage|cache/i.test(key)) + if(ALLOWED && RegExp(PERMISS.join('|'),'i').test(key)) + configuration[key] = options[key]; + else + /* Do nothing */; + // else if(/(^cache-data|paths|qualities)/i.test(key)) + // /* Pre-parse JSON - make sure anything accessing thedata handles objects too */ + // configuration[key] = JSON.parse(options[key] || null); + else + /* Simple copy */ + configuration[key] = options[key]; + + CAUGHT = JSON.parse(options.__caught); + CAUGHT.bump = async(ids) => { + for(let id in ids) + CAUGHT[id.toLowerCase().slice(0, 4)].push(ids[id]); + + let __caught = JSON.stringify(CAUGHT); + + await UTILS_STORAGE.set({ __caught }, () => configuration.__caught = __caught); + }; + + return __CONFIG__ = options; + }, + error => { + new Notification( + 'warning', + 'Fill in missing Web to Plex options', + 15000, + Options + ); + throw error; + } + ); + } + + await ParsedOptions(); + + let AUTO_GRAB = { + ENABLED: __CONFIG__.UseAutoGrab, + LIMIT: __CONFIG__.AutoGrabLimit, + }, + UTILS_DEVELOPER = __CONFIG__.DeveloperMode, // = { true: Developer Mode, fase: Standard Mode } + UTILS_TERMINAL = + UTILS_DEVELOPER? + console: + { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m, honor: m => m }; + + UTILS_TERMINAL.honor = UTILS_TERMINAL.honor? + UTILS_TERMINAL.honor: + (...messages) => { + if(messages.length == 1) { + let message = messages[0], + type = typeof message == 'object'? 'o': 'c'; + + UTILS_TERMINAL.log( + (type == 'o'? message: `%${ type }>> ${ message } <<`), + ( + type == 'o'? + null: + ` + background-color: #00332b; + border-bottom: 1px solid #0000; + border-top: 1px solid #065; + box-sizing: border-box; + clear: right; + color: #f5f5f5; + display: block !important; + line-height: 2; + user-select: text; + + flex-basis: 1; + flex-shrink: 1; + + margin: 0; + overflow-wrap: break-word; + pading: 3px 22px 1px 0; + position: fixed; + z-index: -1; + + min-height: 0; + min-width: 100%; + height: 100%; + width: 100%; + ` + ) + ); + } else { + messages.forEach(message => UTILS_TERMINAL.honor(message)); + } + }; + + if(configuration) { + let host = location.host.replace(/^(ww\w+\.)/, ''), + doms = configuration.__domains.split(','); + + if(!~doms.indexOf(host)) + return; + } + + UTILS_TERMINAL.log('UTILS_DEVELOPER:', UTILS_DEVELOPER, configuration); + + // parse the formatted headers and URL + function HandleProxyHeaders(Headers = "", URL = "") { + let headers = {}; + + Headers.replace(/^[ \t]*([^\=\s]+)[ \t]*=[ \t]*((["'`])(?:[^\\\3]*|\\.)\3|[^\f\n\r\v]*)/gm, ($0, $1, $2, $3, $$, $_) => { + let string = !!$3; + + if(string) { + headers[$1] = $2.replace(RegExp(`^${ $3 }|${ $3 }$`, 'g'), ''); + } else { + $2 = $2.replace(/@([\w\.]+)/g, (_0, _1, _$, __) => { + let path = _1.split('.'), property = top; + + for(let index = 0, length = path.length; index < length; index++) + property = property[path[index]]; + + headers[$1] = property; + }) + .replace(/@\{b(ase-?)?64-url\}/gi, btoa(URL)) + .replace(/@\{enc(ode)?-url\}/gi, encodeURIComponent(URL)) + .replace(/@\{(raw-)?url\}/gi, URL); + } + }); + + return headers; + } + + // fetch/search for the item's media ID(s) + // rerun enum - [0bWXYZ] - [Tried Different URL | Tried Matching Title | Tried Loose Searching | Tried Rerunning Altogether] + async function Identify({ title, alttitle, year, type, IMDbID, TMDbID, TVDbID, APIType, APIID, meta, rerun }) { + let json = {}, // returned object + data = {}, // mutated object + promise, // query promise + api = { + tmdb: __CONFIG__.TMDbAPI || '37930f472ee15263f0b1ef5cc72e181a', + omdb: __CONFIG__.OMDbAPI || 'PlzBanMe', + ombi: __CONFIG__.ombiToken, + }, + apit = APIType || type, // api type (depends on "rqut") + apid = APIID || null, // api id + iid = IMDbID || null, // IMDbID + mid = TMDbID || null, // TMDbID + tid = TVDbID || null, // TVDbID + rqut = apit, // request type: tmdb, imdb, or tvdb + manable = __CONFIG__.ManagerSearch && !(rerun & 0b1000), // is the user's "Manager Searches" option enabled? + UTF_16 = /[^0\u0020-\u007e, 1\u00a1\u00bf-\u00ff, 2\u0100-\u017f, 3\u0180-\u024f, 4\u0300-\u036f, 5\u0370-\u03ff, 6\u0400-\u04ff, 7\u0500-\u052f, 8\u20a0-\u20bf]+/g; + + type = type || null; + meta = { ...meta, mode: 'cors' }; + rqut = + /(tv|show|series)/i.test(rqut)? + 'tvdb': + /(movie|film|cinema)s?/i.test(rqut)? + 'tmdb': + rqut || '*'; + manable = manable && (__CONFIG__.usingOmbi || (__CONFIG__.usingRadarr && rqut == 'tmdb') || ((__CONFIG__.usingSonarr || __CONFIG__.usingMedusa /*|| __CONFIG__.usingSickBeard*/) && rqut == 'tvdb')); + title = (title? title.replace(/\s*[\:,]\s*seasons?\s+\d+.*$/i, '').toCaps(): "") + .replace(/[\u2010-\u2015]/g, '-') // fancy hyphen + .replace(/[\u201a\u275f]/g, ',') // fancy comma + .replace(/[\u2018\u2019\u201b\u275b\u275c`]/g, "'") // fancy apostrophe (tilde from anime results by TMDb) + .replace(/[\u201c-\u201f\u275d\u275e]/g, '"') // fancy quotation marks + .replace(UTF_16, ''); // only accept "usable" characters + /* 0[ -~], 1[¡¿-ÿ], 2[Ā-ſ], 3[ƀ-ɏ], 4[ò-oͯ], 5[Ͱ-Ͽ], 6[Ѐ-ӿ], 7[Ԁ-ԯ], 8[₠-₿] */ + /** Symbol Classes + 0) Basic Latin, and standard characters + 1) Latin (Supplement) + 2) Latin Extended I + 3) Latin Extended II + 4) Diatrical Marks + 5) Greek & Coptic + 6) Basic Cyrillic + 7) Cyrillic (Supplement) + 8) Currency Symbols + */ + year = year? (year + '').replace(/\D+/g, ''): year; + + let plus = (string, character = '+') => string.replace(/\s+/g, character); + + let local, savename; + + if(year) { + savename = `${title} (${year}).${rqut}`.toLowerCase(), + local = await load(savename); + } else { + year = await load(`${title}.${rqut}`.toLowerCase()) || year; + savename = `${title} (${year}).${rqut}`.toLowerCase(); + local = await load(savename); + } + + if(local) { + UTILS_TERMINAL.honor('[LOCAL] Search results', local); + return local; + } + + /* the rest of this function is a beautiful mess that will need to be dealt with later... but it works */ + let url = + (manable && title && __CONFIG__.usingOmbi)? + `${ __CONFIG__.ombiURLRoot }api/v1/Search/${ (rqut == 'imdb' || rqut == 'tmdb' || apit == 'movie')? 'movie': 'tv' }/${ plus(title, '%20') }/?apikey=${ api.ombi }`: + (manable && (__CONFIG__.usingRadarr || __CONFIG__.usingSonarr || __CONFIG__.usingMedusa /*|| __CONFIG__.usingSickBeard*/))? + (__CONFIG__.usingRadarr && (rqut == 'imdb' || rqut == 'tmdb'))? + (mid)? + `${ __CONFIG__.radarrURLRoot }api/movie/lookup/tmdb?tmdbId=${ mid }&apikey=${ __CONFIG__.radarrToken }`: + (iid)? + `${ __CONFIG__.radarrURLRoot }api/movie/lookup/imdb?imdbId=${ iid }&apikey=${ __CONFIG__.radarrToken }`: + `${ __CONFIG__.radarrURLRoot }api/movie/lookup?term=${ plus(title, '%20') }&apikey=${ __CONFIG__.radarrToken }`: + (__CONFIG__.usingSonarr)? + (tid)? + `${ __CONFIG__.sonarrURLRoot }api/series/lookup?term=tvdb:${ tid }&apikey=${ __CONFIG__.sonarrToken }`: + `${ __CONFIG__.sonarrURLRoot }api/series/lookup?term=${ plus(title, '%20') }&apikey=${ __CONFIG__.sonarrToken }`: + (__CONFIG__.usingMedusa)? + (tid)? + `${ __CONFIG__.medusaURLRoot }api/v2/series/tvdb${ tid }?detailed=true&api_key=${ __CONFIG__.medusaToken }`: + `${ __CONFIG__.medusaURLRoot }api/v2/internal/searchIndexersForShowName?query=${ plus(title) }&indexerId=0&api_key=${ __CONFIG__.medusaToken }`: + /* TODO: find a way to get CORS to work on Sick Beard URLs (localhost) */ + // (__CONFIG__.usingSickBeard)? + // (tid)? + // `${ __CONFIG__.sickBeardURLRoot }api/${ __CONFIG__.sickBeardToken }/?cmd=sb.searchtvdb&tvdbid=${ tid }`: + // `${ __CONFIG__.sickBeardURLRoot }api/${ __CONFIG__.sickBeardToken }/?cmd=sb.searchtvdb&name=${ encodeURIComponent(title) }`: + null: + (rqut == 'imdb' || (rqut == '*' && !iid && title) || (rqut == 'tvdb' && !iid && title && !(rerun & 0b1000)) && (rerun |= 0b1000))? + (iid)? + `https://www.omdbapi.com/?i=${ iid }&apikey=${ api.omdb }`: + (year)? + `https://www.omdbapi.com/?t=${ plus(title) }&y=${ year }&apikey=${ api.omdb }`: + `https://www.omdbapi.com/?t=${ plus(title) }&apikey=${ api.omdb }`: + (rqut == 'tmdb' || (rqut == '*' && !mid && title && year) || apit == 'movie')? + (apit && apid)? + `https://api.themoviedb.org/3/${ apit }/${ apid }?api_key=${ api.tmdb }`: + (iid)? + `https://api.themoviedb.org/3/find/${ iid || mid || tid }?api_key=${ api.tmdb }&external_source=${ iid? 'imdb': mid? 'tmdb': 'tvdb' }_id`: + `https://api.themoviedb.org/3/search/${ apit }?api_key=${ api.tmdb }&query=${ encodeURI(title) }${ year? '&year=' + year: '' }`: + (rqut == 'tvdb' || (rqut == '*' && !tid && title) || (apid == tid))? + (tid)? + `https://api.tvmaze.com/shows/?thetvdb=${ tid }`: + (iid)? + `https://api.tvmaze.com/shows/?imdb=${ iid }`: + `https://api.tvmaze.com/search/shows?q=${ encodeURI(title) }`: + (title)? + (apit && year)? + `https://www.theimdbapi.org/api/find/${ apit }?title=${ encodeURI(title) }&year=${ year }`: + `https://www.theimdbapi.org/api/find/movie?title=${ encodeURI(title) }${ year? '&year=' + year: '' }`: + null; + + if(url === null) return null; + + let proxy = __CONFIG__.proxy, + cors = proxy.url, // if cors is requried and not uspported, proxy through this URL + headers = HandleProxyHeaders(proxy.headers, url); + + if(proxy.enabled && /(^http:\/\/)(?!localhost|127\.0\.0\.1(?:\/8)?|::1(?:\/128)?|:\d+)\b/i.test(url)) { + url = cors + .replace(/\{b(ase-?)?64-url\}/gi, btoa(url)) + .replace(/\{enc(ode)?-url\}/gi, encodeURIComponent(url)) + .replace(/\{(raw-)?url\}/gi, url); + + UTILS_TERMINAL.log({ proxy, url, headers }); + } + + UTILS_TERMINAL.log(`Searching for "${ title } (${ year })" in ${ type || apit }/${ rqut }${ proxy.enabled? '[PROXY]': '' } => ${ url }`); + + await(proxy.enabled? fetch(url, { mode: "cors", headers }): fetch(url)) + .then(response => response.text()) + .then(data => { + try { + if(data) + json = JSON.parse(data); + } catch(error) { + UTILS_TERMINAL.warn(`Failed to parse JSON: "${ data }"`); + } + }) + .catch(error => { + throw error; + }); + + UTILS_TERMINAL.honor('Search results', { title, year, url, json }); + + /* DO NOT change to else-if, won't work with Sick Beard: { data: { results: ... } } */ + if('data' in json) + json = json.data; + if('results' in json) + json = json.results; + + if(json instanceof Array) { + let b = { release_date: '', year: '' }, + t = (s = "") => s.toLowerCase(), + c = (s = "") => t(s).replace(/\&/g, 'and').replace(UTF_16, ''), + k = (s = "") => { + + let r = [ + [/(?!^\s*)\b(show|series|a([st]|nd?|cross|fter|lthough)?|b(e(cause|fore|tween)|ut|y)|during|from|in(to)?|[io][fn]|[fn]?or|the|[st]o|through|under|with(out)?|yet)\b\s*/gi, ''], + // try replacing common words, e.g. Conjunctions, "Show," "Series," etc. + [/\^\s*|\s*$/g, ''], + [/\s+/g, '|'], + [/[\u2010-\u2015]/g, '-'], // fancy hyphen + [/[\u201a\u275f]/g, ','], // fancy comma + [/[\u2018\u2019\u201b\u275b\u275c`]/g, "'"], // fancy apostrophe (tilde from anime results by TMDb) + [/[\u201c-\u201f\u275d\u275e]/g, '"'], // fancy quotation marks + [/'(?=\B)|\B'/g, ''] + ]; + + for(let i = 0; i < r.length; i++) { + if(/^([\(\|\)]+)?$/.test(s)) return ""; + + s = s.replace(r[i][0], r[i][1]); + } + + return c(s); + }, + R = (s = "", S = "", n = !0) => { + let l = s.split(' ').length, L = S.split(' ').length, E, + score = 100 * (((S.match(E = RegExp(`\\b(${k(s)})\\b`, 'gi')) || [null]).length) / (L || 1)), + passing = __CONFIG__.UseLooseScore | 0; + + UTILS_TERMINAL.log(`\tQuick Match => "${ s }"/"${ S }" = ${ score }% (${ E })`); + score *= (l > L? (L||1)/l: L > l? (l||1)/L: 1); + UTILS_TERMINAL.log(`\tActual Match (${ passing }% to pass) ~> ... = ${ score }%`); + + return (S != '' && score >= passing) || (n? R(S, s, !n): n); + }, + en = /^(u[ks]-?|utf8-?)?en(glish)?$/i; + + // Find an exact match: Title (Year) | #IMDbID + let index, found, $data, lastscore; + for(index = 0, found = false, $data, lastscore = 0; (title && year) && index < json.length && !found; rerun |= 0b0100, index++) { + $data = json[index]; + + let altt = $data.alternativeTitles, + $alt = (altt && altt.length? altt.filter(v => t(v) == t(title))[0]: null); + + // Managers + if(manable) + // Medusa + if(__CONFIG__.usingMedusa && $data instanceof Array) + found = ((t($data[4]) == t(title) || $alt) && +year == +$data[5].slice(0, 4))? + $alt || $data: + found; + // Radarr & Sonarr + else if(__CONFIG__.usingRadarr || __CONFIG__.usingSonarr) + found = ((t($data.title) == t(title) || $alt) && +year == +$data.year)? + $alt || $data: + found; + // Sick Beard + else if(__CONFIG__.usingSickBeard) + found = ((t($data.name) == t(title) || $alt) && +year == parseInt($data.first_aired))? + $alt || $data: + found; + //api.tvmaze.com/ + else if(('externals' in ($data = $data.show || $data) || 'show' in $data) && $data.premiered) + found = (iid == $data.externals.imdb || t($data.name) == t(title) && year == $data.premiered.slice(0, 4))? + $data: + found; + //api.themoviedb.org/ \local + else if(('movie_results' in $data || 'tv_results' in $data || 'results' in $data) && $data.release_date) + found = (DATA => { + if(DATA.results) + if(rqut == 'tmdb') + DATA.movie_results = DATA.results; + else + DATA.tv_results = DATA.results; + + let i, f, o, l; + + for(i = 0, f = !1, o = DATA.movie_results, l = o.length | 0; i < l; i++) + f = (t(o.title) == t(title) && o.release_date.slice(0, 4) == year); + + for(i = (+f * l), o = (f? o: DATA.tv_results), l = (f? l: o.length | 0); i < l; i++) + f = (t(o.name) == t(title) && o.first_air_date.slice(0, 4) == year); + + return f? o: f = !!iid; + })($data); + //api.themoviedb.org/ \remote + else if(('original_name' in $data || 'original_title' in $data) && $data.release_date) + found = (tid == $data.id || (t($data.original_name || $data.original_title) == t(title) || t($data.name) == t(title)) && year == ($data || b).release_date.slice(0, 4))? + $data: + found; + //theimdbapi.org/ + else if($data.release_date) + found = (t($data.title) == t(title) && year == ($data.url || $data || b).release_date.slice(0, 4))? + $data: + found; + + UTILS_TERMINAL.log(`Strict Matching: ${ !!found }`, !!found? found: null); + } + + // Find a close match: Title + for(index = 0; title && index < json.length && (!found || lastscore > 0); rerun |= 0b0100, index++) { + $data = json[index]; + + let altt = $data.alternativeTitles, + $alt = (altt && altt.length? altt.filter(v => c(v) == c(title)): null); + + // Managers + if(manable) + // Medusa + if(__CONFIG__.usingMedusa && $data instanceof Array) + found = (c($data[4]) == c(title) || $alt)? + $alt || $data: + found; + // Radarr & Sonarr + if(__CONFIG__.usingRadarr || __CONFIG__.usingSonarr) + found = (c($data.title) == c(title) || $alt)? + $alt || $data: + found; + // Sick Beard + if(__CONFIG__.usingSickBeard) + found = (c($data.name) == c(title) || $alt)? + $alt || $data: + found; + //api.tvmaze.com/ + else if('externals' in ($data = $data.show || $data) || 'show' in $data) + found = + // ignore language barriers + (c($data.name) == c(title))? + $data: + // trust the api matching + ($data.score > lastscore)? + (lastscore = $data.score || $data.vote_count, $data): + found; + //api.themoviedb.org/ \local + else if('movie_results' in $data || 'tv_results' in $data || 'results' in $data) + found = (DATA => { + let i, f, o, l; + + if(DATA.results) + if(rqut == 'tmdb') + DATA.movie_results = DATA.results; + else + DATA.tv_results = DATA.results; + + for(i = 0, f = !1, o = DATA.movie_results, l = o.length | 0; i < l; i++) + f = (c(o.title) == c(title)); + + for(i = (+f * l), o = (f? o: DATA.tv_results), l = (f? l: o.length | 0); i < l; i++) + f = (c(o.name) == c(title)); + + return f? o: f; + })($data); + //api.themoviedb.org/ \remote + else if('original_name' in $data || 'original_title' in $data || 'name' in $data) + found = (c($data.original_name || $data.original_title || $data.name) == c(title))? + $data: + found; + //theimdbapi.org/ + else if(en.test($data.language)) + found = (c($data.title) == c(title))? + $data: + found; + + UTILS_TERMINAL.log(`Title Matching: ${ !!found }`, !!found? found: null); + } + + // Find an OK match (Loose Searching): Title ~ Title + // The examples below are correct + // GOOD, found: VRV's "Bakemonogatari" vs. TVDb's "Monogatari Series" + // /\b(monogatari)\b/i.test('bakemonogatari') === true + // this is what this option was designed for + // OK, found: "The Title of This is Bad" vs. "The Title of This is OK" (this is semi-errornous) + // /\b(title|this|bad)\b/i.test('title this ok') === true + // this may be a possible match, but it may also be an error: 'title' and 'this' + // the user's defined threshold is used in this case (above 65% would match these two items) + // BAD, not found: "Gun Show Showdown" vs. "Gundarr" + // /\b(gun|showdown)\b/i.test('gundarr') === false + // this should not match; the '\b' (border between \w and \W) keeps them from matching + for(index = 0; __CONFIG__.UseLoose && title && index < json.length && (!found || lastscore > 0); rerun |= 0b0010, index++) { + $data = json[index]; + + let altt = $data.alternativeTitles, + $alt = (altt && altt.length? altt.filter(v => R(v, title)): null); + + // Managers + if(manable) + // Medusa + if(__CONFIG__.usingMedusa && $data instanceof Array) + found = (R($data[4], title) || $alt)? + $alt || $data: + found; + // Radarr & Sonarr + if(__CONFIG__.usingRadarr || __CONFIG__.usingSonarr) + found = (R($data.name || $data.title, title) || $alt)? + $alt || $data: + found; + // Sick Beard + if(__CONFIG__.usingSickBeard) + found = (R($data.name, title) || $alt)? + $alt || $data: + found; + //api.tvmaze.com/ + else if('externals' in ($data = $data.show || $data) || 'show' in $data) + found = + // ignore language barriers + (R($data.name, title) || UTILS_TERMINAL.log('Matching:', [$data.name, title], R($data.name, title)))? + $data: + // trust the api matching + ($data.score > lastscore)? + (lastscore = $data.score, $data): + found; + //api.themoviedb.org/ \local + else if('movie_results' in $data || 'tv_results' in $data) + found = (DATA => { + let i, f, o, l; + + for(i = 0, f = !1, o = DATA.movie_results, l = o.length | 0; i < l; i++) + f = R(o.title, title); + + for(i = (+f * l), o = (f? o: DATA.tv_results), l = (f? l: o.length | 0); i < l; i++) + f = R(o.name, title); + + return f? o: f; + })($data); + //api.themoviedb.org/ \remote + else if('original_name' in $data || 'original_title' in $data) + found = (R($data.original_name, title) || R($data.original_title, title) || R($data.name, title))? + $data: + found; + //theimdbapi.org/ + else if(en.test($data.language)) + found = (R($data.title, title))? + $data: + found; + + UTILS_TERMINAL.log(`Loose Matching: ${ !!found }`, !!found? found: null); + } + + json = found; + } + + if((json === undefined || json === null || json === false) && !(rerun & 0b0001)) + return UTILS_TERMINAL.warn(`Trying to find "${ title }" again (as "${ (alttitle || title) }")`), rerun |= 0b0001, json = Identify({ title: (alttitle || title), year: YEAR, type, IMDbID, TMDbID, TVDbID, APIType, APIID, meta, rerun }); + else if((json === undefined || json === null)) + json = { IMDbID, TMDbID, TVDbID }; + + let ei = 'tt', + mr = 'movie_results', + tr = 'tv_results'; + + json = json && mr in json? json[mr].length > json[tr].length? json[mr]: json[tr]: json; + + if(json instanceof Array && (!__CONFIG__.usingMedusa? true: (__CONFIG__.usingSonarr || __CONFIG__.usingOmbi || __CONFIG__.usingSickBeard))) + json = json[0]; + + if(!json) + json = { IMDbID, TMDbID, TVDbID }; + + // Ombi, Medusa, Radarr and Sonarr + if(manable) + data = ( + (__CONFIG__.usingMedusa && !(__CONFIG__.usingSonarr || __CONFIG__.usingOmbi || __CONFIG__.usingSickBeard))? + { + imdb: iid || ei, + tmdb: mid | 0, + tvdb: tid || json[3] || (json[8]? json[8][1]: 0), + title: json.title || title, + year: +(json.year || year) + }: + { + imdb: iid || json.imdbId || ei, + tmdb: mid || json.tmdbId || json.theMovieDbId | 0, + tvdb: tid || json.tvdbId || json.theTvDbId | 0, + title: json.title || title, + year: +(json.year || year) + } + ); + //api.tvmaze.com/ + else if('externals' in (json = json.show || json)) + data = { + imdb: iid || json.externals.imdb || ei, + tmdb: mid || json.externals.themoviedb | 0, + tvdb: tid || json.externals.thetvdb | 0, + title: json.name || title, + year: ((json.premiered || json.first_aired_date || year) + '').slice(0, 4) + }; + //api.themoviedb.org/ + else if('imdb_id' in (json = mr in json? json[mr].length > json[tr].length? json[mr]: json[tr]: json) || 'original_name' in json || 'original_title' in json) + data = { + imdb: iid || json.imdb_id || ei, + tmdb: mid || json.id | 0, + tvdb: tid || json.tvdb | 0, + title: json.title || json.name || title, + year: ((json.release_date || json.first_air_date || year) + '').slice(0, 4) + }; + //omdbapi.com/ + else if('imdbID' in json) + data = { + imdb: iid || json.imdbID || ei, + tmdb: mid || json.tmdbID | 0, + tvdb: tid || json.tvdbID | 0, + title: json.Title || json.Name || title, + year: json.Year || year + }; + //theapache64.com/movie_db/ + else if('data' in json) + data = { + imdb: iid || json.data.imdb_id || ei, + tmdb: mid || json.data.tmdb_id | 0, + tvdb: tid || json.data.tvdb_id | 0, + title: json.data.name || json.data.title || title, + year: json.data.year || year + }; + //theimdbapi.org/ + else if('imdb' in json) + data = { + imdb: iid || json.imdb || ei, + tmdb: mid || json.id | 0, + tvdb: tid || json.tvdb | 0, + title, + year + }; + // given by the requesting service + else + data = { + imdb: iid || ei, + tmdb: mid | 0, + tvdb: tid | 0, + title, + year + }; + + year = +((data.year + '').slice(0, 4)) || 0; + data.year = year; + + let best = { title, year, data, type, rqut, score: json.score | 0 }; + + UTILS_TERMINAL.log('Best match:', url, { best, json }); + + if(best.data.imdb == ei && best.data.tmdb == 0 && best.data.tvdb == 0) + return UTILS_TERMINAL.log(`No information was found for "${ title } (${ year })"`), {}; + + save(savename, data); // e.g. "Coco (0)" on Netflix before correction / no repeat searches + save(savename = `${title} (${year}).${rqut}`.toLowerCase(), data); // e.g. "Coco (2017)" on Netflix after correction / no repeat searches + save(`${title}.${rqut}`.toLowerCase(), year); + + UTILS_TERMINAL.log(`Saved as "${ savename }"`, data); + + rerun |= 0b00001; + + return data; + } + + function Request_CouchPotato(options) { + // TODO: this does not work anymore! + if(!options.IMDbID) + return new Notification( + 'warning', + 'Stopped adding to CouchPotato: No IMDb ID' + ); + + browser.runtime.sendMessage({ + type: 'VIEW_COUCHPOTATO', + url: `${ __CONFIG__.couchpotatoURL }/media.get`, + IMDbID: options.IMDbID, + TMDbID: options.TMDbID, + TVDbID: options.TVDbID, + basicAuth: __CONFIG__.couchpotatoBasicAuth, + }).then(response => { + let movieExists = response.success; + if(response.error) { + return new Notification( + 'warning', + 'CouchPotato request failed (see your console)' + ) || + (!response.silent && UTILS_TERMINAL.warn('Error viewing CouchPotato: ' + String(response.error))); + } + if(!movieExists) { + __Request_CouchPotato__(options); + return; + } + new Notification( + 'warning', + `Movie already exists in CouchPotato (status: ${response.status})` + ); + }, error => { + new Notification( + 'warning', + error + ); + + throw error; + }); + } + + // Movies/TV Shows + function Request_Ombi(options) { + new Notification('info', `Sending "${ options.title }" to Ombi`, 3000); + + if((!options.IMDbID && !options.TMDbID) && !options.TVDbID) { + return new Notification( + 'warning', + 'Stopped adding to Ombi: No content ID' + ); + } + + let contentType = (/movies?|film/i.test(options.type)? 'movie': 'tv'); + + browser.runtime.sendMessage({ + type: 'PUSH_OMBI', + url: `${ __CONFIG__.ombiURL }api/v1/Request/${ contentType }`, + token: __CONFIG__.ombiToken, + title: options.title, + year: options.year, + imdbId: options.IMDbID, + tmdbId: options.TMDbID, + tvdbId: options.TVDbID, + contentType, + }).then(response => { + UTILS_TERMINAL.log('Pushing to Ombi', response); + + if(response && response.error) { + return new Notification('warning', `Could not add "${ options.title }" to Ombi: ${ response.error }`) || + (!response.silent && UTILS_TERMINAL.warn('Error adding to Ombi: ' + String(response.error), response.location, response.debug)); + } else if(response === true || (response && response.success)) { + let title = options.title.replace(/\&/g, 'and').replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-{2,}/g, '-').toLowerCase(), + { IMDbID, TMDbID, TVDbID } = options; + + CAUGHT.bump({ IMDbID, TMDbID, TVDbID }); + + UTILS_TERMINAL.honor('Successfully pushed', options); + new Notification('update', `Added "${ options.title }" to Ombi`, 7000, () => window.open(__CONFIG__.ombiURL, '_blank')); + } else { + new Notification('warning', `Could not add "${ options.title }" to Ombi: Unknown Error`) || + (!response.silent && UTILS_TERMINAL.warn('Error adding to Ombi: ' + String(response))); + } + }, error => { + new Notification( + 'warning', + error + ); + + throw error; + }); + } + + // Movies/TV Shows + function __Request_CouchPotato__(options) { + new Notification('info', `Sending "${ options.title }" to CouchPotato`, 3000); + + browser.runtime.sendMessage({ + type: 'PUSH_COUCHPOTATO', + url: `${ __CONFIG__.couchpotatoURL }/movie.add`, + IMDbID: options.IMDbID, + TMDbID: options.TMDbID, + TVDbID: options.TVDbID, + basicAuth: __CONFIG__.couchpotatoBasicAuth, + }).then(response => { + UTILS_TERMINAL.log('Pushing to CouchPotato', response); + + if(response.error) { + return new Notification( + 'warning', + `Could not add "${ options.title }" to CouchPotato (see your console)` + ) || + (!response.silent && UTILS_TERMINAL.warn('Error adding to CouchPotato: ' + String(response.error), response.location, response.debug)); + } + if(response.success) { + let { IMDbID, TMDbID, TVDbID } = options; + + CAUGHT.bump({ IMDbID, TMDbID, TVDbID }); + + UTILS_TERMINAL.honor('Successfully pushed', options); + new Notification('update', `Added "${ options.title }" to CouchPotato`); + } else { + new Notification('warning', `Could not add "${ options.title }" to CouchPotato`); + } + }, error => { + new Notification( + 'warning', + error + ); + + throw error; + }); + } + + // Movies + function Request_Watcher(options) { + new Notification('info', `Sending "${ options.title }" to Watcher`, 3000); + + if(!options.IMDbID && !options.TMDbID) { + return new Notification( + 'warning', + 'Stopped adding to Watcher: No IMDb/TMDb ID' + ); + } + + browser.runtime.sendMessage({ + type: 'PUSH_WATCHER', + url: `${ __CONFIG__.watcherURL }api/`, + token: __CONFIG__.watcherToken, + StoragePath: __CONFIG__.watcherStoragePath, + basicAuth: __CONFIG__.watcherBasicAuth, + title: options.title, + year: options.year, + imdbId: options.IMDbID, + tmdbId: options.TMDbID, + }).then(response => { + UTILS_TERMINAL.log('Pushing to Watcher', response); + + if(response && response.error) { + return new Notification('warning', `Could not add "${ options.title }" to Watcher: ${ response.error }`) || + (!response.silent && UTILS_TERMINAL.warn('Error adding to Watcher: ' + String(response.error), response.location, response.debug)); + } else if(response && (response.success || (response.response + "") == "true")) { + let title = options.title.replace(/\&/g, 'and').replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-{2,}/g, '-').toLowerCase(), + TMDbID = options.TMDbID || response.tmdbId, + IMDbID = options.IMDbID || response.imdbId; + + CAUGHT.bump({ IMDbID, TMDbID }); + + UTILS_TERMINAL.honor('Successfully pushed', options); + new Notification('update', `Added "${ options.title }" to Watcher`, 7000, () => window.open(`${__CONFIG__.watcherURL}library/status${TMDbID? `#${title}-${TMDbID}`: '' }`, '_blank')); + } else { + new Notification('warning', `Could not add "${ options.title }" to Watcher: Unknown Error`) || + (!response.silent && UTILS_TERMINAL.warn('Error adding to Watcher: ' + String(response))); + } + }, error => { + new Notification( + 'warning', + error + ); + + throw error; + }); + } + + // Movies + function Request_Radarr(options, prompted) { + if(!options.IMDbID && !options.TMDbID) + return (!prompted)? new Notification( + 'warning', + 'Stopped adding to Radarr: No IMDb/TMDb ID' + ): null; + + let PromptValues = {}, + { PromptQuality, PromptLocation } = __CONFIG__; + + if(!prompted && (PromptQuality || PromptLocation)) + return new Prompt('modify', options, refined => Request_Radarr(refined, true)); + + if(PromptQuality && +options.quality > 0) + PromptValues.QualityID = +options.quality; + if(PromptLocation && options.location) + PromptValues.StoragePath = JSON.parse(__CONFIG__.radarrStoragePaths).map(item => item.id == options.location? item: null).filter(n => n)[0].path.replace(/\\/g, '\\\\'); + + new Notification('info', `Sending "${ options.title }" to Radarr`, 3000); + + browser.runtime.sendMessage({ + type: 'PUSH_RADARR', + url: `${ __CONFIG__.radarrURL }api/movie/`, + token: __CONFIG__.radarrToken, + StoragePath: __CONFIG__.radarrStoragePath, + QualityID: __CONFIG__.radarrQualityProfileId, + basicAuth: __CONFIG__.radarrBasicAuth, + title: options.title, + year: options.year, + imdbId: options.IMDbID, + tmdbId: options.TMDbID, + ...PromptValues + }).then(response => { + UTILS_TERMINAL.log('Pushing to Radarr', response); + + if(response && response.error) { + return new Notification('warning', `Could not add "${ options.title }" to Radarr: ${ response.error }`) || + (!response.silent && UTILS_TERMINAL.warn('Error adding to Radarr: ' + String(response.error), response.location, response.debug)); + } else if(response === true || (response && response.success)) { + let title = options.title.replace(/\&/g, 'and').replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-{2,}/g, '-').toLowerCase(), + TMDbID = options.TMDbID || response.tmdbId, + IMDbID = options.IMDbID || response.imdbId; + + CAUGHT.bump({ IMDbID, TMDbID }); + + UTILS_TERMINAL.honor('Successfully pushed', options); + new Notification('update', `Added "${ options.title }" to Radarr`, 7000, () => window.open(`${__CONFIG__.radarrURL}${TMDbID? `movies/${title}-${TMDbID}`: '' }`, '_blank')); + } else { + new Notification('warning', `Could not add "${ options.title }" to Radarr: Unknown Error [${ String(response) }]`) || + (!response.silent && UTILS_TERMINAL.warn('Error adding to Radarr: ' + String(response))); + } + }, error => { + new Notification( + 'warning', + error + ); + + throw error; + }); + } + + // TV Shows + function Request_Sonarr(options, prompted) { + if(!options.TVDbID) + return (!prompted)? new Notification( + 'warning', + 'Stopped adding to Sonarr: No TVDb ID' + ): null; + + let PromptValues = {}, + { PromptQuality, PromptLocation } = __CONFIG__; + + if(!prompted && (PromptQuality || PromptLocation)) + return new Prompt('modify', options, refined => Request_Sonarr(refined, true)); + + if(PromptQuality && +options.quality > 0) + PromptValues.QualityID = +options.quality; + if(PromptLocation && options.location) + PromptValues.StoragePath = JSON.parse(__CONFIG__.sonarrStoragePaths).map(item => item.id == options.location? item: null).filter(n => n)[0].path.replace(/\\/g, '\\\\'); + + new Notification('info', `Sending "${ options.title }" to Sonarr`, 3000); + + browser.runtime.sendMessage({ + type: 'PUSH_SONARR', + url: `${ __CONFIG__.sonarrURL }api/series/`, + token: __CONFIG__.sonarrToken, + StoragePath: __CONFIG__.sonarrStoragePath, + QualityID: __CONFIG__.sonarrQualityProfileId, + basicAuth: __CONFIG__.sonarrBasicAuth, + title: options.title, + year: options.year, + tvdbId: options.TVDbID, + ...PromptValues + }).then(response => { + UTILS_TERMINAL.log('Pushing to Sonarr', response); + + if(response && response.error) { + return new Notification('warning', `Could not add "${ options.title }" to Sonarr: ${ response.error }`) || + (!response.silent && UTILS_TERMINAL.warn('Error adding to Sonarr: ' + String(response.error), response.location, response.debug)); + } else if(response === true || (response && response.success)) { + let title = options.title.replace(/\&/g, 'and').replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-{2,}/g, '-').toLowerCase(), + TVDbID = options.TVDbID || response.tvdbId; + + CAUGHT.bump({ TVDbID }); + + UTILS_TERMINAL.honor('Successfully pushed', options); + new Notification('update', `Added "${ options.title }" to Sonarr`, 7000, () => window.open(`${__CONFIG__.sonarrURL}series/${title}`, '_blank')); + } else { + new Notification('warning', `Could not add "${ options.title }" to Sonarr: Unknown Error`) || + (!response.silent && UTILS_TERMINAL.warn('Error adding to Sonarr: ' + String(response))); + } + }, error => { + new Notification( + 'warning', + error + ); + + throw error; + }); + } + + // TV Shows + function Request_Medusa(options, prompted) { + if(!options.TVDbID) + return (!prompted)? new Notification( + 'warning', + 'Stopped adding to Medusa: No TVDb ID' + ): null; + + let PromptValues = {}, + { PromptQuality, PromptLocation } = __CONFIG__; + + if(!prompted && (PromptQuality || PromptLocation)) + return new Prompt('modify', options, refined => Request_Medusa(refined, true)); + + if(PromptQuality && +options.quality > 0) + PromptValues.QualityID = +options.quality; + if(PromptLocation && options.location) + PromptValues.StoragePath = JSON.parse(__CONFIG__.medusaStoragePaths).map(item => item.id == options.location? item: null).filter(n => n)[0].path.replace(/\\/g, '\\\\'); + + new Notification('info', `Sending "${ options.title }" to Medusa`, 3000); + + browser.runtime.sendMessage({ + type: 'PUSH_MEDUSA', + url: `${ __CONFIG__.medusaURL }api/v2/series`, + root: `${ __CONFIG__.medusaURL }api/v2/`, + token: __CONFIG__.medusaToken, + StoragePath: __CONFIG__.medusaStoragePath, + QualityID: __CONFIG__.medusaQualityProfileId, + basicAuth: __CONFIG__.medusaBasicAuth, + title: options.title, + year: options.year, + tvdbId: options.TVDbID, + ...PromptValues + }).then(response => { + UTILS_TERMINAL.log('Pushing to Medusa', response); + + if(response && response.error) { + return new Notification('warning', `Could not add "${ options.title }" to Medusa: ${ response.error }`) || + (!response.silent && UTILS_TERMINAL.warn('Error adding to Medusa: ' + String(response.error), response.location, response.debug)); + } else if(response === true || (response && response.success)) { + let title = options.title.replace(/\&/g, 'and').replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-{2,}/g, '-').toLowerCase(), + TVDbID = options.TVDbID || response.tvdbId; + + CAUGHT.bump({ TVDbID }); + + UTILS_TERMINAL.honor('Successfully pushed', options); + new Notification('update', `Added "${ options.title }" to Medusa`, 7000, () => window.open(`${__CONFIG__.medusaURL}home/displayShow?indexername=tvdb&seriesid=${options.TVDbID}`, '_blank')); + } else { + new Notification('warning', `Could not add "${ options.title }" to Medusa: Unknown Error`) || + (!response.silent && UTILS_TERMINAL.warn('Error adding to Medusa: ' + String(response))); + } + }, error => { + new Notification( + 'warning', + error + ); + + throw error; + }); + } + + // TV Shows + function Request_SickBeard(options, prompted) { + if(!options.TVDbID) + return (!prompted)? new Notification( + 'warning', + 'Stopped adding to Sick Beard: No TVDb ID' + ): null; + + let PromptValues = {}, + { PromptQuality, PromptLocation } = __CONFIG__; + + if(!prompted && (PromptQuality || PromptLocation)) + return new Prompt('modify', options, refined => Request_SickBeard(refined, true)); + + if(PromptQuality && +options.quality > 0) + PromptValues.QualityID = +options.quality; + if(PromptLocation && +options.location >= 0) + PromptValues.StoragePath = JSON.parse(__CONFIG__.sickBeardStoragePaths)[+options.location].path.replace(/\\/g, '\\\\'); + + new Notification('info', `Sending "${ options.title }" to Sick Beard`, 3000); + + browser.runtime.sendMessage({ + type: 'PUSH_SICKBEARD', + url: `${ __CONFIG__.sickBeardURL }api/${ __CONFIG__.sickBeardToken }/`, + token: __CONFIG__.sickBeardToken, + StoragePath: __CONFIG__.sickBeardStoragePath, + QualityID: __CONFIG__.sickBeardQualityProfileId, + basicAuth: __CONFIG__.sickBeardBasicAuth, + title: options.title, + year: options.year, + tvdbId: options.TVDbID, + exists: !!~JSON.parse(__CONFIG__.__caught).tvdb.indexOf(options.TVDbID), + ...PromptValues + }).then(response => { + UTILS_TERMINAL.log('Pushing to Sick Beard', response); + + if(response && response.error) { + return new Notification('warning', `Could not add "${ options.title }" to Sick Beard: ${ response.error }`) || + (!response.silent && UTILS_TERMINAL.warn('Error adding to Sick Beard: ' + String(response.error), response.location, response.debug)); + } else if(response === true || (response && response.success)) { + let title = options.title.replace(/\&/g, 'and').replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-{2,}/g, '-').toLowerCase(), + TVDbID = options.TVDbID || response.tvdbId; + + CAUGHT.bump({ TVDbID }); + + UTILS_TERMINAL.honor('Successfully pushed', options); + new Notification('update', `Added "${ options.title }" to Sick Beard`, 7000, () => window.open(`${__CONFIG__.sickBeardURL}home/displayShow?show=${ TVDbID }`, '_blank')); + } else { + new Notification('warning', `Could not add "${ options.title }" to Sick Beard: Unknown Error`) || + (!response.silent && UTILS_TERMINAL.warn('Error adding to Sick Beard: ' + String(response))); + } + }, error => { + new Notification( + 'warning', + error + ); + + throw error; + }); + } + + // make the button + // ( PERSISTENT, { HEADER_CLASSES } ) + let MASTER_BUTTON; + function RenderButton(persistent, headers = {}) { + let existingButtons = document.queryBy('.web-to-plex-button'), + firstButton = existingButtons.first; + + if(existingButtons.length && !persistent) + [].slice.call(existingButtons).forEach(button => button.remove()); + else if(persistent && firstButton !== null && firstButton !== undefined) + return firstButton; + + let ThemeClasses = JSON.parse(__CONFIG__.__theme), + HeaderClasses = []; + + // Theme(s) + if(!ThemeClasses.length) + ThemeClasses = ''; + else + ThemeClasses = '.' + ThemeClasses.join('.'); + + // Header(s) + for(let header in headers) + if(headers[header]) + HeaderClasses.push( header ); + + if(!HeaderClasses.length) + HeaderClasses = ''; + else + HeaderClasses = '.' + HeaderClasses.join('.'); + + // + + document.body.appendChild(button); + + return MASTER_BUTTON = button; + } + + function UpdateButton(button, action, title, options = {}) { + if(!button) + return /* Rare, but happens: especially on failed download links sent*/; + + let multiple = (action == 'multiple' || options instanceof Array), + element = button.querySelector('.w2p-action, .list-action'), + delimeter = '', + ty = 'Item', txt = 'title', hov = 'tooltip', + em = /^(tt|0)?$/i, + tv = /tv[\s-]?|shows?|series/i; + + if(!element) { + element = button; + button = element.parentElement; + }; + + Update('SEARCH_FOR', { ...options, button }); + + /* Handle a list of items */ + if(multiple) { + options = [].slice.call(options); + + let saved_options = [], // a list of successful searches (not on Plex) + len = options.length, + s = (len == 1? '': 's'), + t = []; + + for(let index = 0; index < len; index++) { + let option = options[index]; + + // Skip empty entries + if(!option || !option.type || !option.title) + continue; + + // Skip queued entries + if( + !!~CAUGHT.imdb.indexOf(option.IMDbID) || + !!~CAUGHT.tmdb.indexOf(option.TMDbID) || + !!~CAUGHT.tvdb.indexOf(option.TVDbID) + ) + continue; + + // the action should be an array + // we'll give the button a list of links to engage, so make it snappy! + let url = `#${ option.IMDbID || 'tt' }-${ option.TMDbID | 0 }-${ option.TVDbID | 0 }`; + + /* Failed */ + if(/#tt-0-0/i.test(url)) + continue; + + saved_options.push(option); + t.push(option.title); + } + + t = t.join(', '); + t = t.length > 24? t.slice(0, 21).replace(/\W+$/, '') + '...': t; + + element.ON_CLICK = e => { + e.preventDefault(); + + let self = e.target, tv = /tv[\s-]?|shows?|series/i, fail = 0, + options = JSON.parse(decode(button.getAttribute('saved_options'))); + + for(let index = 0, length = options.length, option; index < length; index++) { + option = options[index]; + + try { + if(__CONFIG__.usingOmbi) + Request_Ombi(option, true); + else if(__CONFIG__.usingWatcher && !tv.test(option.type)) + Request_Watcher(option, true); + else if(__CONFIG__.usingRadarr && !tv.test(option.type)) + Request_Radarr(option, true); + else if(__CONFIG__.usingCouchPotato && !tv.test(option.type)) + Request_CouchPotato(option, true); + else if(__CONFIG__.usingSonarr && tv.test(option.type)) + Request_Sonarr(option, true); + else if(__CONFIG__.usingMedusa && tv.test(option.type)) + Request_Medusa(option, true); + else if(__CONFIG__.usingSickBeard && tv.test(option.type)) + Request_SickBeard(option, true); + + button.classList.replace('wtp--download', 'wtp--queued'); + } catch(error) { + UTILS_TERMINAL.warn(`Failed to get "${ option.title }" (Error #${ ++fail })`) + } + } + NOTIFIED = false; + + if(fail) + new Notification('error', `Failed to grab ${ fail } item${fail==1?'':'s'}`); + }; + + button.setAttribute('saved_options', encode(JSON.stringify(saved_options))); + element.addEventListener('click', e => (AUTO_GRAB.ENABLED && AUTO_GRAB.LIMIT > options.length)? element.ON_CLICK(e): new Prompt('select', options, o => { button.setAttribute('saved_options', encode(JSON.stringify(o))); element.ON_CLICK(e) })); + + element.setAttribute(hov, `Grab ${len} new item${s}: ${ t }`); + button.classList.add(saved_options.length || len? 'wtp--download': 'wtp--error'); + } else { + /* Handle a single item */ + + if(!options || !options.type || !options.title) + return; + + let empty = (em.test(options.IMDbID) && em.test(options.TMDbID) && em.test(options.TVDbID)), + nice_title = `${options.title.toCaps()}${options.year? ` (${options.year})`: ''}`; + + if(options) { + ty = (/^(cine(ma)?|films?|movies?|theat[re]{2})$/i.test(options.type)? 'Movie': 'TV Show'); + txt = options.txt || txt; + hov = options.hov || hov; + } + + if(action == 'found') { + element.href = Request_PlexURL(__CONFIG__.server.id, options.key); + element.setAttribute(hov, `Watch "${options.title} (${options.year})" on Plex`); + button.classList.add('wtp--found'); + + new Notification('success', `Watch "${ nice_title }"`, 7000, e => element.click(e)); + } else if(action == 'downloader' || options.remote) { + + switch(options.remote) { + /* Vumoo & GoStream */ + case 'plex': + case 'oload': + case 'consistent': + let href = options.href, path = ''; + + if(__CONFIG__.usingOmbi) { + path = ''; + } else if(__CONFIG__.usingWatcher && !tv.test(options.type)) { + path = ''; + } else if(__CONFIG__.usingRadarr && !tv.test(options.type)) { + path = __CONFIG__.radarrStoragePath; + } else if(__CONFIG__.usingSonarr && tv.test(options.type)) { + path = __CONFIG__.sonarrStoragePath; + } else if(__CONFIG__.usingMedusa && tv.test(options.type)) { + path = __CONFIG__.medusaStoragePath; + } else if(__CONFIG__.usingSickBeard && tv.test(options.type)) { + path = __CONFIG__.sickBeardStoragePath; + } else if(__CONFIG__.usingCouchPotato) { + path = ''; + } + + element.href = `#${ options.IMDbID || 'tt' }-${ options.TMDbID | 0 }-${ options.TVDbID | 0 }`; + + button.classList.remove('wtp--queued'); + button.classList.add('wtp--download'); + + element.removeEventListener('click', element.ON_CLICK); + element.addEventListener('click', element.ON_DOWNLOAD = e => { + e.preventDefault(); + + Update('DOWNLOAD_FILE', { ...options, button, href, path }); + new Notification('update', 'Opening prompt (may take a while)...'); + }); + + element.setAttribute(hov, `Download "${ nice_title }" | ${ty}`); + Update('SAVE_AS', { ...options, button, href, path }); + new Notification('update', `"${ nice_title }" can be downloaded`, 7000, e => element.click(e)); + return; + + + /* Default & Error */ + default: + let url = `#${ options.IMDbID || 'tt' }-${ options.TMDbID | 0 }-${ options.TVDbID | 0 }`; + + /* Failed */ + if(/#tt-0-0/i.test(url)) + return UpdateButton(button, 'notfound', title, options); + + element.href = url; + button.classList.add('wtp--download'); + element.addEventListener('click', element.ON_CLICK = e => { + e.preventDefault(); + try { + if(__CONFIG__.usingOmbi) + Request_Ombi(options); + else if(__CONFIG__.usingWatcher && !tv.test(options.type)) + Request_Watcher(options); + else if(__CONFIG__.usingRadarr && !tv.test(options.type)) + Request_Radarr(options); + else if(__CONFIG__.usingCouchPotato && !tv.test(options.type)) + Request_CouchPotato(options); + else if(__CONFIG__.usingSonarr && tv.test(options.type)) + Request_Sonarr(options); + else if(__CONFIG__.usingMedusa && tv.test(options.type)) + Request_Medusa(options); + else if(__CONFIG__.usingSickBeard && tv.test(options.type)) + Request_SickBeard(options); + + button.classList.replace('wtp--download', 'wtp--queued'); + } catch(error) { + throw error; + } + + }); + } + NOTIFIED = false; + + element.setAttribute(hov, `Add "${ nice_title }" | ${ty}`); + element.style.removeProperty('display'); + } else if(action == 'notfound' || action == 'error' || empty) { + element.removeAttribute('href'); + + empty = !(options && options.title); + + if(empty) + element.setAttribute(hov, `${ty || 'Item'} not found`); + else + element.setAttribute(hov, `"${ nice_title }" was not found`); + + button.classList.remove('wtp--found'); + button.classList.add('wtp--error'); + } + + if((action == 'downloader') && (!!~CAUGHT.imdb.indexOf(options.IMDbID) || !!~CAUGHT.tmdb.indexOf(options.TMDbID) || !!~CAUGHT.tvdb.indexOf(options.TVDbID))) { + element.setAttribute(hov, `Modify "${ nice_title }" | ${ty}`); + + button.classList.remove('wtp--found'); + button.classList.add('wtp--queued'); + } + + element.id = options? `${options.IMDbID || 'tt'}-${options.TMDbID | 0}-${options.TVDbID | 0}`: 'tt-0-0'; + } + } + + // Find media on Plex + async function FindMediaItems(options, button) { + UTILS_TERMINAL.log(`Finding items... ${ JSON.stringify({ options, button }) }`); + + if(!(options && options.length && button)) + return; + + let results = [], + length = options.length, + queries = (FindMediaItems.queries = FindMediaItems.queries || {}); + + FindMediaItems.OPTIONS = options; + + let query = JSON.stringify(options); + + query = (queries[query] = queries[query] || {}); + + if(query.running === true) + return; + else if(query.results) { + let { results, multiple, items } = query; + + new Notification('update', `Welcome back. ${ multiple } new ${ items } can be grabbed`, 7000, (event, target = button.querySelector('.list-action')) => target.click({ ...event, target })); + + if(multiple) + UpdateButton(button, 'multiple', `Download ${ multiple } ${ items }`, results); + + return; + } + + query.running = true; + + new Notification('info', `Processing ${ length } item${ 's'[+(length === 1)] || '' }...`); + + for(let index = 0, option, opt; index < length; index++) { + let { IMDbID, TMDbID, TVDbID } = (option = await options[index]); + + opt = { name: option.title, title: option.title, year: option.year, image: options.image, type: option.type, imdb: IMDbID, IMDbID, tmdb: TMDbID, TMDbID, tvdb: TVDbID, TVDbID }; + + try { + await Request_Plex(option) + .then(async({ found, key }) => { + if(found) { + // ignore found items, we only want new items + } else { + option.field = 'original_title'; + + return await Request_Plex(option) + .then(({ found, key }) => { + if(found) { + // ignore found items, we only want new items + } else { + let available = (__CONFIG__.usingOmbi || __CONFIG__.usingWatcher || __CONFIG__.usingRadarr || __CONFIG__.usingSonarr || __CONFIG__.usingMedusa || __CONFIG__.usingSickBeard || __CONFIG__.usingCouchPotato), + action = (available ? 'downloader' : 'notfound'), + title = available ? + 'Not on Plex (download available)': + 'Not on Plex (download not available)'; + + results.push({ ...opt, found: false, status: action }); + } + }); + } + }) + } catch(error) { + UTILS_TERMINAL.warn('Request to Plex failed: ' + String(error)); + // new Notification('error', 'Failed to query item #' + (index + 1)); + } + } + + results = results.filter(v => v.status == 'downloader'); + + let img = furnish('img', { title: 'Add to Plex It!', src: IMG_URL.plexit_icon_48, onmouseup: event => {let frame = document.querySelector('#plexit-bookmarklet-frame'); frame.src = frame.src.replace(/(#plexit:.*)?$/, '#plexit:' + event.target.parentElement.getAttribute('data'))} }), + po, pi = furnish('li#plexit.list-item', { data: encode(JSON.stringify(results)) }, img), + op = document.querySelector('#wtp-plexit'); + + if(po = button.querySelector('#plexit')) + po.remove(); + try { + button.querySelector('ul').insertBefore(pi, op); + } catch(e) { /* Don't do anything */ } + + let multiple = results.length, + items = multiple == 1? 'item': 'items'; + + new Notification('update', `Done. ${ multiple } new ${ items } can be grabbed`, 7000, (event, target = button.querySelector('.list-action')) => target.click({ ...event, target })); + + query.running = false; + query.results = results; + query.multiple = multiple; + query.items = items; + + POPULATING = false; + + if(multiple) + UpdateButton(button, 'multiple', `Download ${ multiple } ${ items }`, results); + } + + async function FindMediaItem(options) { + UTILS_TERMINAL.log(`Finding item... ${ JSON.stringify(options) }`); + + if(!(options && options.title)) + return; + + let { IMDbID, TMDbID, TVDbID } = options; + + TMDbID = +TMDbID; + TVDbID = +TVDbID; + + let opt = { name: options.title, year: options.year, image: options.image || IMG_URL.nil, type: options.type, imdb: IMDbID, IMDbID, tmdb: TMDbID, TMDbID, tvdb: TVDbID, TVDbID }, + op = document.querySelector('#wtp-plexit'), + img = (options.image)? + furnish('div', { tooltip: 'Add to Plex It!', style: `background: url(${ IMG_URL.plexit_icon_16 }) top right/60% no-repeat, #0004 url(${ opt.image }) center/contain no-repeat; height: 48px; width: 34px;`, draggable: true, onmouseup: event => {let frame = document.querySelector('#plexit-bookmarklet-frame'); frame.src = frame.src.replace(/(#plexit:.*)?$/, '#plexit:' + event.target.parentElement.getAttribute('data'))} }): + furnish('img', { title: 'Add to Plex It!', src: IMG_URL.plexit_icon_48, onmouseup: event => {let frame = document.querySelector('#plexit-bookmarklet-frame'); frame.src = frame.src.replace(/(#plexit:.*)?$/, '#plexit:' + event.target.parentElement.getAttribute('data'))} }); + + FindMediaItem.OPTIONS = options; + + try { + return Request_Plex(options) + .then(({ found, key }) => { + POPULATING = false; + + if(found) { + UpdateButton(options.button, 'found', 'On Plex', { ...options, key }); + opt = { ...opt, url: options.button.href, found: true, status: 'found' }; + + let po, pi = furnish('li#plexit.list-item', { data: encode(JSON.stringify(opt)) }, img); + + if(po = options.button.querySelector('#plexit')) + po.remove(); + try { + options.button.querySelector('ul').insertBefore(pi, op); + } catch(e) { /* Don't do anything */ } + } else { + options.field = 'original_title'; + + return Request_Plex(options) + .then(({ found, key }) => { + POPULATING = false; + + if(found) { + UpdateButton(options.button, 'found', 'On Plex', { ...options, key }); + opt = { ...opt, url: options.button.href, found: true, status: 'found' }; + + let po, pi = furnish('li#plexit.list-item', { data: encode(JSON.stringify(opt)) }, img); + + if(po = options.button.querySelector('#plexit')) + po.remove(); + try { + options.button.querySelector('ul').insertBefore(pi, op); + } catch(e) { /* Don't do anything */ } + } else { + let available = (__CONFIG__.usingOmbi || __CONFIG__.usingWatcher || __CONFIG__.usingRadarr || __CONFIG__.usingSonarr || __CONFIG__.usingMedusa || __CONFIG__.usingSickBeard || __CONFIG__.usingCouchPotato), + action = (available ? 'downloader' : 'notfound'), + title = available ? + 'Not on Plex (download available)': + 'Not on Plex (download not available)'; + + UpdateButton(options.button, action, title, options); + opt = { ...opt, found: false, status: action }; + + let po, pi = furnish('li#plexit.list-item', { data: encode(JSON.stringify(opt)) }, img); + + if(po = options.button.querySelector('#plexit')) + po.remove(); + if(!!~[].slice.call(options.button.querySelector('ul').children).indexOf(op)) + try { + options.button.querySelector('ul').insertBefore(pi, op); + } catch(e) { /* Don't do anything */ } + } + return found; + }); + } + return found; + }) + } catch(error) { + return UpdateButton( + options.button, + 'error', + 'Request to Plex Media Server failed', + options + ), + UTILS_TERMINAL.warn(`Request to Plex failed: ${ String(error) }`), + false; + // new Notification('Failed to communicate with Plex'); + } + } + + function Request_Plex(options) { + UTILS_TERMINAL.log('Requesting Plex search...', options); + + if(!(__CONFIG__.plexURL && __CONFIG__.plexToken) || __CONFIG__.IGNORE_PLEX) + return new Promise((resolve, reject) => resolve({ found: false, key: null })); + + return new Promise((resolve, reject) => { + UTILS_TERMINAL.log('Searching Plex...'); + + options = JSON.parse( JSON.stringify(options) ); + + UTILS_TERMINAL.log('Sanitized options:', options); + + browser.runtime.sendMessage({ + type: 'SEARCH_PLEX', + options, + serverConfig: __CONFIG__.server + }).then(response => { + if(response && response.error) { + UTILS_TERMINAL.warn(`ERROR: ${ response.error }`); + reject(response.error); + } else if(!response) { + UTILS_TERMINAL.warn(`ERROR: Unknown Error`); + reject(new Error('Unknown error')); + } else { + UTILS_TERMINAL.log(`RESPONSE:`, response); + resolve(response); + } + }); + }); + } + + function Request_PlexURL(PlexUIID, key) { + return __CONFIG__.plexURL.replace(RegExp(`\/(${ __CONFIG__.server.id })?$`), `/web#!/server/` + PlexUIID) + `/details?key=${encodeURIComponent( key )}`; + } + + /* Listen for events */ + let POPULATING = false; + + browser.runtime.onMessage.addListener(async(request, sender, callback) => { + UTILS_TERMINAL.log(`Listener event [${ request.instance_type }#${ request[request.instance_type.toLowerCase()] }]:`, request); + + let data = request.data, + LOCATION = `${ request.name || 'anonymous' } @ instance ${ request.instance }`, + PARSING_ERROR = `Can't parse missing information. ${ LOCATION }`, + BUTTON_ERROR = `The button failed to render. ${ LOCATION }`, + EMPTY_REQUEST = `The given request is empty. ${ LOCATION }`; + + UTILS_TERMINAL.log(`Assuming data... ${ JSON.stringify(data) }`); + + if(!data) + return UTILS_TERMINAL.warn(EMPTY_REQUEST), false; + let button = RenderButton(true); + + if(!button) + return UTILS_TERMINAL.warn(BUTTON_ERROR), false; + button.classList.remove('sleeper'); + + UTILS_TERMINAL.log(`Switching request... ${ JSON.stringify(request) }`); + + switch(request.type) { + case 'POPULATE': + + if(POPULATING) + return UTILS_TERMINAL.log(`Already attempting to populate...`), false; + else + UTILS_TERMINAL.log(`Populating... ${ JSON.stringify(data) }`); + + POPULATING = true; + + if(data instanceof Array) { + for(let index = 0, length = data.length, item; index < length; index++) + if(!(item = data[index]) || !item.type) + data.splice(index, 1, null); + + data = data.filter(value => value !== null && value !== undefined); + + for(let index = 0, length = data.length, item; index < length; index++) { + let { image, type, title, year, IMDbID, TMDbID, TVDbID } = (item = data[index]); + + if(!item.title || !item.type) + continue; + + let Db = await Identify(item); + + IMDbID = IMDbID || Db.imdb || 'tt'; + TMDbID = TMDbID || Db.tmdb || 0; + TVDbID = TVDbID || Db.tvdb || 0; + + title = title || Db.title; + year = +(year || Db.year || 0); + + data.splice(index, 1, { type, title, year, image, button, IMDbID, TMDbID, TVDbID }); + } + + if(!data.length) + return UTILS_TERMINAL.warn(PARSING_ERROR), false; + else + FindMediaItems(data, button); + } else { + if(!data || !data.title || !data.type) + return UTILS_TERMINAL.warn(PARSING_ERROR), false; + + let { image, type, title, year, IMDbID, TMDbID, TVDbID } = data; + let Db = await Identify(data); + + IMDbID = IMDbID || Db.imdb || 'tt'; + TMDbID = TMDbID || Db.tmdb || 0; + TVDbID = TVDbID || Db.tvdb || 0; + + title = title || Db.title; + year = +(year || Db.year || 0); + + let found = await FindMediaItem({ type, title, year, image, button, IMDbID, TMDbID, TVDbID }); + Update('FOUND', { ...request, found }, true); + } + return true; + + case 'INITIALIZE': + init && init(); + return true; + + case 'NO_RENDER': + document.queryBy('.web-to-plex-button').map(e => e.remove()); + return true; + + default: + // UTILS_TERMINAL.warn(`Unknown event [${ request.type }]`); + return false; + } + }); + + /* Listen for Window events - from iframes, etc. */ + top.addEventListener('message', async request => { + try { + request = request.data; + + switch(request.type) { + case 'SEND_VIDEO_LINK': + let options = { ...FindMediaItem.OPTIONS, href: request.href, remote: request.from }; + + UTILS_TERMINAL.log(`Download Event [${ options.remote }]:`, options); + + UpdateButton(MASTER_BUTTON, 'downloader', 'Download', options); + return true; + + case 'NOTIFICATION': + let { state, text, timeout = 7000, callback = () => {}, requiresClick = true } = request.data; + new Notification(state, text, timeout, callback, requiresClick); + return true; + + case 'PERMISSION': + let { data } = request; + + if(typeof data.allowed == 'boolean') { + ALLOWED = data.allowed; + PERMISS = data.allotted; + + await ParsedOptions(); + + (init && !RUNNING? (init(), RUNNING = true): RUNNING = false); + } else { + UTILS_TERMINAL.warn('Permission Request:', data); + new Prompt('permission', data); + } + return true; + + default: + // UTILS_TERMINAL.warn(`Unknown event [${ request.type }]`); + return false; + } + } catch(error) { + new Notification('error', `Unable to use downloader: ${ String(error) }`); + throw error + } + }); + + // create the sleeping button + wait(() => document.readyState === 'complete', () => RenderButton(false, { sleeper: true })); + +})(new Date); + +/* Helpers */ + +function wait(on, then) { + if(on && ((on instanceof Function && on()) || true)) + then && then(); + else + setTimeout(() => wait(on, then), 50); +} + +// the custom "on location change" event +function watchlocationchange(subject) { + let locationchangecallbacks = watchlocationchange.locationchangecallbacks; + + watchlocationchange[subject] = watchlocationchange[subject] || location[subject]; + + if(watchlocationchange[subject] != location[subject]) { + let from = watchlocationchange[subject], + to = location[subject], + properties = { from, to }, + sign = code => (code + '').replace(/\W+/g, '').toLowerCase(); + + watchlocationchange[subject] = location[subject]; + + for(let index = 0, length = locationchangecallbacks.length, callback, exists, signature; length > 0 && index < length; index++) { + callback = locationchangecallbacks[index]; + exists = locationchangecallbacks.exists[signature = sign(callback)]; + + let event = new Event('locationchange', { bubbles: true }); + + if(!exists && typeof callback == 'function') { + /* The eventlistener does not exist */ + window.addEventListener('beforeunload', event => { + event.preventDefault(false); + callback({ event, ...properties }); + }); + } else { + /* The eventlistener already exists */ + callback({ event, ...properties }); + } + + open(to, '_self'); + } + } +} +watchlocationchange.locationchangecallbacks = watchlocationchange.locationchangecallbacks || []; +watchlocationchange.locationchangecallbacks.exists = watchlocationchange.locationchangecallbacks.exists || {}; + +if(!('onlocationchange' in window)) + Object.defineProperty(window, 'onlocationchange', { + set: callback => { + if(typeof callback != 'function') + return null; + + let signature = (callback + '').replace(/\W+/g, '').toLowerCase(); + + if(!watchlocationchange.locationchangecallbacks.exists[signature]) { + watchlocationchange.locationchangecallbacks.exists[signature] = true; + + return watchlocationchange.locationchangecallbacks.push(callback); + } + return null; + }, + get: () => watchlocationchange.locationchangecallbacks + }); + +watchlocationchange.onlocationchangeinterval = watchlocationchange.onlocationchangeinterval || setInterval(() => watchlocationchange('href'), 1000); +// at least 1s is needed to properly fire the event ._. + +String.prototype.toCaps = String.prototype.toCaps || function toCaps(all) { + /** Titling Caplitalization + * Articles: a, an, & the + * Conjunctions: and, but, for, nor, or, so, & yet + * Prepositions: across, after, although, at, because, before, between, by, during, from, if, in, into, of, on, to, through, under, with, & without + */ + let array = this.toLowerCase(), + titles = /(?!^|(?:an?|the)\s+)\b(a([st]|nd?|cross|fter|lthough)?|b(e(cause|fore|tween)?|ut|y)|during|from|in(to)?|[io][fn]|[fn]?or|the|[st]o|through|under|with(out)?|yet)(?!\s*$)\b/gi, + cap_exceptions = /([\|\"\(]\s*[a-z]|[\:\.\!\?]\s+[a-z]|(?:^\b|[^\'\-\+]\b)[^aeiouy\d\W]+\b)/gi, // Punctuation exceptions, e.g. "And not I" + all_exceptions = /\b((?:ww)?(?:m{1,4}(?:c?d(?:c{0,3}(?:x?l(?:x{0,3}(?:i?vi{0,3})?)?)?)?)?|c?d(?:c{0,3}(?:x?l(?:x{0,3}(?:i?vi{0,3})?)?)?)?|c{1,3}(?:x?l(?:x{0,3}(?:i?vi{0,3})?)?)?|x?l(?:x{0,3}(?:i?vi{0,3})?)?|x{1,3}(?:i?vi{0,3})?|i?vi{0,3}|i{1,3}))\b/gi, // Roman Numberals + cam_exceptions = /\b((?:mr?s|[sdjm]r|mx)|(?:adm|cm?dr?|chf|c[op][lmr]|cpt|gen|lt|mjr|sgt)|doc|hon|prof)(?:\.|\b)/gi, // Titles (Most Common?) + low_exceptions = /'([\w]+)/gi; // Apostrphe cases + + array = array.split(/\s+/); + + let index, length, string, word; + for(index = 0, length = array.length, string = [], word; index < length; index++) { + word = array[index]; + + if(word) + string.push( word[0].toUpperCase() + word.slice(1, word.length) ); + } + + string = string.join(' '); + + if(!all) + string = string + .replace(titles, ($0, $1, $$, $_) => $1.toLowerCase()) + .replace(all_exceptions, ($0, $1, $$, $_) => $1.toUpperCase()) + .replace(cap_exceptions, ($0, $1, $$, $_) => $1.toUpperCase()) + .replace(low_exceptions, ($0, $1, $$, $_) => $0.toLowerCase()) + .replace(cam_exceptions, ($0, $1, $$, $_) => $1[0].toUpperCase() + $1.slice(1, $1.length).toLowerCase() + '.'); + + return string; +}; + +(function(parent) { +/* SortBy.js */ +/** Usage + Example + // document.queryBy( selectors )... + + let index = 0; + // the order given is the order handled + document.queryBy("div:last-child, div:nth-child(2), div:first-child") + .forEach((element, index, array) => element.innerHTML = index + 1); + + // output w/sortBySelector: +
3
+
2
+
1
+ + // output w/o sortBySelector: +
1
+
2
+
3
+ */ + parent.queryBy = parent.queryBy || function queryBy(selectors, container = parent) { + // Helpers + let copy = array => [...array], + query = (SELECTORS, CONTAINER = container) => CONTAINER.querySelectorAll(SELECTORS); + + // Get rid of enclosing syntaxes: [...] and (...) + let regexp = /(\([^\(\)]+?\)|\[[^\[\]]+?\])/g, + pulled = [], + media = [], + index, length; + + // The index shouldn't be longer than the length of the selector's string + // Keep this to prevent infinite loops + for(index = 0, length = selectors.length; index++ < length && regexp.test(selectors);) + selectors = selectors.replace(regexp, ($0, $1, $$, $_) => '\b--' + pulled.push($1) + '\b'); + + let order = selectors.split(','), + dummy = copy(order), + output = [], + generations = 0; + + // Replace those syntaxes (they were ignored) + for(index = 0, length = dummy.length, order = [], regexp = /[\b]--(\d+)[\b]/g; index < length; index++) + order.push(dummy[index].replace(regexp, ($0, $1, $$, $_) => pulled[+$1 - 1])); + + // Make sure to put the elements in order + // Handle the :parent (pseudo) selector + for(index = 0, length = order.length; index < length; generations = 0, index++) { + let selector = order[index], ancestor; + + selector = selector + .replace(/\:nth-parent\((\d+)\)/g, ($0, $1, $$, $_) => (generations -= +$1, '')) + .replace(/(\:{1,2}parent\b|<\s*(\*|\s*(,|$)))/g, ($0, $$, $_) => (--generations, '')) + .replace(/<([^<,]+)?/g, ($0, $1, $$, $_) => (ancestor = $1, --generations, '')) + .replace(/^\s+|\s+$/g, ''); + + let elements = query(selector), + parents = [], parent; + + for(; generations < 0; generations++) + elements.forEach( element => { + let P = element, Q = P.parentElement, R = (Q? Q.parentElement: {}), + E = C => [...query(ancestor, C)], + F, G; + + for(let I = 0, L = -generations; ancestor && !!R && !!Q && !!P && I < L; I++) + parent = !!~E(R).indexOf(Q)? Q: G; + + for(let I = 0, L = -generations; !!Q && !!P && I < L; I++) + parent = Q = (P = Q).parentElement; + + if(!~parents.indexOf(parent)) + parents.push(parent); + }); + media.push(parents.length? parents: elements); + } + + // Create a continuous array from the sub-arrays + for(index = 1, length = media.length; index < length; index++) + media.splice(0, 1, copy(media[0]).concat( copy(media[index]) )); + output = [].slice.call(media[0]).filter( value => value ); + + // Remove repeats + for(index = 0, length = output.length, media = []; index < length; index++) + if(!~media.indexOf(output[index])) + media.push(output[index]); + + let properties = { writable: false, enumerable: false, configurable: false }; + + Object.defineProperties(media, { + first: { + value: media[0], + ...properties + }, + last: { + value: media[media.length - 1], + ...properties + }, + child: { + value: index => media[index - 1], + ...properties + }, + empty: { + value: !media.length, + ...properties + }, + }); + + return media; + }; + +/** Adopted from + * LICENSE: MIT (2018) + */ + parent.furnish = parent.furnish || function furnish(TAGNAME, ATTRIBUTES = {}, ...CHILDREN) { + let u = v => v && v.length, R = RegExp, name = TAGNAME, attributes = ATTRIBUTES, children = CHILDREN; + + if( !u(name) ) + throw TypeError(`TAGNAME cannot be ${ (name === '')? 'empty': name }`); + + let options = attributes.is === true? { is: true }: null; + + delete attributes.is; + + name = name.split(/([#\.][^#\.\[\]]+)/).filter( u ); + + if(name.length <= 1) + name = name[0].split(/^([^\[\]]+)(\[.+\])/).filter( u ); + + if(name.length > 1) + for(let n = name, i = 1, l = n.length, t, v; i < l; i++) + if((v = n[i].slice(1, n[i].length)) && (t = n[i][0]) == '#') + attributes.id = v; + else if(t == '.') + attributes.classList = [].slice.call(attributes.classList || []).concat(v); + else if(/\[(.+)\]/.test(n[i])) + R.$1.split('][').forEach(N => attributes[(N = N.replace(/\s*=\s*(?:("?)([^]*)\1)?/, '=$2').split('=', 2))[0]] = N[1] || ''); + name = name[0]; + + let element = document.createElement(name, options); + + if(attributes.classList instanceof Array) + attributes.classList = attributes.classList.join(' '); + + Object.entries(attributes).forEach( + ([name, value]) => (/^(on|(?:(?:inner|outer)(?:HTML|Text)|textContent|class(?:List|Name)|value)$)/.test(name))? + (typeof value == 'string' && /^on/.test(name))? + (() => { + try { + /* Can't make a new function(eval) */ + element[name] = new Function('', value); + } catch (__error) { + try { + /* Not a Chrome (extension) state */ + browser.tabs.getCurrent(tab => browser.tabs.executeScript(tab.id, { code: `document.furnish.__cache__ = () => {${ value }}` }, __cache__ => element[name] = __cache__[0] || parent.furnish.__cache__ || value)); + } catch (_error) { + throw __error, _error; + } + } + })(): + element[name] = value: + element.setAttribute(name, value) + ); + + children + .filter( child => child !== undefined && child !== null ) + .forEach( + child => + child instanceof Element? + element.append(child): + child instanceof Node? + element.appendChild(child): + element.appendChild( + parent.createTextNode(child) + ) + ); + + return element; + } +})(document); + +let PRIMITIVE = Symbol.toPrimitive, + queryBy = document.queryBy, + furnish = document.furnish; + +queryBy[PRIMITIVE] = furnish[PRIMITIVE] = String.prototype.toCaps[PRIMITIVE] = () => "function () { [foreign code] }"; diff --git a/moz/verizon$.js b/moz/verizon$.js new file mode 100644 index 0000000..b7294b1 --- /dev/null +++ b/moz/verizon$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'verizon' }))(); diff --git a/moz/verizon.js b/moz/verizon.js new file mode 100644 index 0000000..8a89d99 --- /dev/null +++ b/moz/verizon.js @@ -0,0 +1,58 @@ +let script = { + "url": "*://*.verizon.com/*/(movie|show)s?/*", + + "ready": () => !$('.container .btn-with-play, .moredetails, .more-like').empty, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let image = $('.cover img').first, + type = script.getType(), + title, year; + + if(script.ondemand) { + if(type == 'movie') { + title = $('.detail *').first; + year = $('.rating *').first; + } else if(type == 'show') { + title = { textContent: top.location.pathname.replace(/\/ondemand\/tvshows?\/([^\/]+?)\/.*/i) }; + year = $('#showDetails > * > *:nth-child(4) *:last-child').first; + + title.textContent = decodeURL(title.textContent).toCpas(); + } else { + return null; + } + } else if(script.watch) { + title = $('[class*="title__"]').first; + year = $('[class*="subtitle__"]').first; + } else { + title = $('.copy > .title').first; + year = (type == 'movie')? + $('.copy > .details').first: + $('.summary ~ .title ~ *').first; + } + + if(!title) + return 1000; + + year = +year.textContent.slice(0, 4).trim(); + title = title.textContent.replace(RegExp(`\\s*\\(${ year }\\).*`), '').trim(); + image = (image || {}).src; + + return { type, title, year, image }; + }, + + "getType": () => { + let { pathname } = top.location; + + return /\bmovies?\b/i.test(pathname)? + 'movie': + /\bseries\b/i.test(pathname)? + 'show': + 'error' + }, + + ondemand: /\bondemand\b/i.test(top.location.pathname), + + watch: /\bwatch\b/i.test(top.location.pathname), +}; diff --git a/moz/verizon.png b/moz/verizon.png new file mode 100644 index 0000000..7ed2596 Binary files /dev/null and b/moz/verizon.png differ diff --git a/moz/vrv$.js b/moz/vrv$.js new file mode 100644 index 0000000..c4a2052 --- /dev/null +++ b/moz/vrv$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'vrv' }))(); diff --git a/moz/vrv.js b/moz/vrv.js new file mode 100644 index 0000000..1e1f7af --- /dev/null +++ b/moz/vrv.js @@ -0,0 +1,80 @@ +let script = { + "url": "*://*.vrv.co/(series|watch)/", + + "ready": () => { + let img = $('.h-thumbnail > img').first, + pre = $('#content .content .card').first; + + return script.getType('list')? pre && pre.textContent: img && img.src; + }, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let type = script.getType(), + title, year, image, options; + + switch(type) { + case 'movie': + case 'show': + title = $('.series, .series-title, .video-title, [class*="series"] .title, [class*="video"] .title').first; + year = $('.additional-information-item').first; + image = $('.series-poster img').first; + + title = title.textContent.replace(/(unrated|mature|tv-?\d{1,2})\s*$/i, '').trim(); + year = year? +year.textContent.replace(/.+(\d{4}).*/, '$1').trim(): 0; + image = (image || {}).src; + + options = { type, title, year, image }; + break; + + case 'list': + let items = $('#content .content .card'); + + options = []; + + items.forEach(element => { + let option = script.process(element); + + if(option) + options.push(option); + }); + break; + + default: + return 5000; + } + + return options; + }, + + "getType": (expected) => { + let type = 'error', + { pathname } = top.location; + + type = (/^\/(?:series)\//.test(pathname) || (/^\/(?:watch)\//.test(pathname) && !$('.content .series').empty))? + 'show': + (/^\/(?:watch)\//.test(pathname) && $('.content .series').empty)? + 'movie': + (/\/(watchlist)\b/i.test(pathname))? + 'list': + type; + + if(expected) + return type == expected; + + return type; + }, + + "process": (element) => { + let title = $('.info > *', element).first, + image = $('.poster-image img', element).first, + type = $('.info [class*="series"], .info [class*="movie"]', element).first; + + title = title.textContent.trim(); + image = image.src; + type = type.getAttribute('class').replace(/[^]*(movie|series)[^]*/, '$1'); + + return { type, title, image }; + }, +}; diff --git a/moz/vrv.png b/moz/vrv.png new file mode 100644 index 0000000..5e82ab4 Binary files /dev/null and b/moz/vrv.png differ diff --git a/moz/vudu$.js b/moz/vudu$.js new file mode 100644 index 0000000..303f5ba --- /dev/null +++ b/moz/vudu$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'vudu' }))(); diff --git a/moz/vudu.js b/moz/vudu.js new file mode 100644 index 0000000..15af40a --- /dev/null +++ b/moz/vudu.js @@ -0,0 +1,32 @@ +let script = { + "url": "*://*.vudu.com/*", + + "ready": () => !$('img[src*="poster" i]').empty, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('.head-big').first, + year = $('.container .row:first-child .row ~ * > .row span').first, + image = $('img[src*="poster" i]').first, + type = script.getType(); + + title = title.textContent.replace(/\((\d{4})\)/, '').trim(); + year = year? year.textContent.split(/\s*\|\s*/): R.$1; + image = (image || {}).src; + + if(!title) + return 5000; + + year = +year[year.length - 1].slice(0, 4); + year |= 0; + + return { type, title, year, image }; + }, + + "getType": () => { + return /(?:Season-\d+\/\d+)$/i.test(window.location.pathname)? + 'show': + 'movie'; + }, +}; diff --git a/moz/vudu.png b/moz/vudu.png new file mode 100644 index 0000000..c68017e Binary files /dev/null and b/moz/vudu.png differ diff --git a/moz/vumoo$.js b/moz/vumoo$.js new file mode 100644 index 0000000..2793ee0 --- /dev/null +++ b/moz/vumoo$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'vumoo' }))(); diff --git a/moz/vumoo.js b/moz/vumoo.js new file mode 100644 index 0000000..1a32142 --- /dev/null +++ b/moz/vumoo.js @@ -0,0 +1,68 @@ +let script = { + "url": "*://*.vumoo.to/(movies|tv-series)/*", + + "ready": () => !$('[role="presentation"i]').empty, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('.film-box h1').first, + year = $('.film-box > * span').filter(e => /\b\d{4}\b/.test(e.textContent))[0], + image = $('.poster').first, + type = script.getType(); + + title = title.textContent.replace(/\s*season\s+\d+\s*$/i, '').replace(/\s*\((\d{4})\)/, '').trim(); + year = +(type == 'movie')? + R.$1: + year.textContent.replace(/[^]*(\d{4})[^]*/, '$1'); + image = (image? image.src: null); + + // auto-prompt downloading for the user + let servers = $('.play'), + roles = $('[role="presentation"i] a'); + + if(servers.length > 1 && type != 'show') { + OLOAD_EVENTS.push(setTimeout( + () => Notify('update', 'Finding download links...', 3000), + 500 + )); + + servers.forEach((server, index, array) => OLOAD_EVENTS.push(setTimeout( + () => { + roles[index].click(); + server.click(); + + if(index == servers.length -1) + OLOAD_EVENTS.push(setTimeout( + () => Notify('update', 'No download links found'), + 7000 + )); + }, + index * 4500 + ))); + } + + return { type, title, year, image }; + }, + + "getType": () => { + let { pathname } = top.location; + + return pathname.startsWith('/movies')? + 'movie': + 'show'; + }, +}, + OLOAD_EVENTS = []; + +top.addEventListener('message', request => { + try { + request = request.data; + + if(request) + if(request.from == 'oload' || request.found == true) + OLOAD_EVENTS.forEach(timeout => clearTimeout(timeout)); + } catch(error) { + throw error; + } +}); diff --git a/moz/vumoo.png b/moz/vumoo.png new file mode 100644 index 0000000..ddf273a Binary files /dev/null and b/moz/vumoo.png differ diff --git a/moz/watcher.png b/moz/watcher.png new file mode 100644 index 0000000..581d81b Binary files /dev/null and b/moz/watcher.png differ diff --git a/moz/webtoplex$.js b/moz/webtoplex$.js new file mode 100644 index 0000000..291f462 --- /dev/null +++ b/moz/webtoplex$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'webtoplex' }))(); diff --git a/moz/webtoplex.js b/moz/webtoplex.js new file mode 100644 index 0000000..9c5063c --- /dev/null +++ b/moz/webtoplex.js @@ -0,0 +1,57 @@ +// optional +// "Web to Plex" requires: api, token +// 'Friendly Name' requires permissions... + +let script = { + // required + "url": "*://ephellon.github.io/web.to.plex/(?!test|login)", + // Example: *://*.amazon.com/*/video/(detail|buy)/* + // *:// - match any protocol (http, https, etc.) + // *.amazon.com - match any sub-domain (www, ww5, etc.) + // /* - match any path + // (detail|buy) - match one of the items + + // optional + "ready": () => configuration && location.search && location.search.length > 1 && $('#tmdb').first.textContent, + + // optional + "timeout": 5000, // if the script fails to complete, retry after ... milliseconds + + // required + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('#title').first, + year = $('#year').first, + image = $('#poster').first, + type = script.getType(), // described below + IMDbID = script.getID('imdb')||"", + TMDbID = script.getID('tmdb')|0; + + title = title.textContent; + year = year.textContent|0; + image = image.src; + + return { type, title, year, image, IMDbID, TMDbID }; + }, + + // optional | functioanlity only + "getType": () => ($('#info').first.getAttribute('type') == 'movie'? 'movie': 'show'), + + "getID": (provider) => $(`#${provider}`).first.textContent, +}; + +(() => { + let login = /\blogin\b/.test(location.pathname), + apikey = $('#apikey').first; + + if(login && configuration.TMDbAPI && !apikey.value) { + apikey.value = configuration.TMDbAPI; + + return -1; + // no longer needed to run + } else if(login) { + return -1; + // don't run on the login page + } +})(); diff --git a/moz/xml.js b/moz/xml.js new file mode 100644 index 0000000..0bff785 --- /dev/null +++ b/moz/xml.js @@ -0,0 +1,89 @@ +/* global _ */ +/* eslint-disable no-unused-vars */ + +// flattens an object (recursively!), similarly to Array#flatten +// e.g. flatten({ a: { b: { c: "hello!" } } }); // => "hello!" +function _flatten(object) { + return (_.isPlainObject(object) && _.size(object) === 1) ? _flatten(_.values(object)[0]) : object; +} + +function _parse(xml) { + let data = {}, + isText = xml.nodeType === 3, + isElement = xml.nodeType === 1, + body = xml.textContent && xml.textContent.trim(), + hasChildren = xml.children && xml.children.length, + hasAttributes = xml.attributes && xml.attributes.length; + + // if it's text just return it + if (isText) { + return xml.nodeValue.trim(); + } + + // if it doesn't have any children or attributes, just return the contents + if (!hasChildren && !hasAttributes) { + return body; + } + + // if it doesn't have children but _does_ have body content, we'll use that + if (!hasChildren && body.length) { + data.text = body; + } + + // if it's an element with attributes, add them to data.attributes + if (isElement && hasAttributes) { + data.attributes = _.reduce( + xml.attributes, + (obj, name, id) => { + const attr = xml.attributes.item(id); + obj[attr.name] = attr.value; + return obj; + }, + {} + ); + } + + // recursively call #parse over children, adding results to data + _.each(xml.children, child => { + const name = child.nodeName; + + // if we've not come across a child with this nodeType, add it as an object + // and return here + if (!_.has(data, name)) { + data[name] = _parse(child); + return; + } + + // if we've encountered a second instance of the same nodeType, make our + // representation of it an array + if (!_.isArray(data[name])) { + data[name] = [data[name]]; + } + + // and finally, append the new child + data[name].push(_parse(child)); + }); + + // if we can, let's fold some attributes into the body + _.each(data.attributes, (value, key) => { + if (data[key] != null) { + return; + } + data[key] = value; + delete data.attributes[key]; + }); + + // if data.attributes is now empty, get rid of it + if (_.isEmpty(data.attributes)) { + delete data.attributes; + } + + // simplify to reduce number of final leaf nodes and return + return _flatten(data); +} + +function parseXML(string) { + let xml = new DOMParser().parseFromString(string, 'text/xml'); + + return _parse(xml); +} diff --git a/moz/yarn.lock b/moz/yarn.lock new file mode 100644 index 0000000..0b0d8c9 --- /dev/null +++ b/moz/yarn.lock @@ -0,0 +1,3973 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@7.0.0-beta.40", "@babel/code-frame@^7.0.0-beta.40": + version "7.0.0-beta.40" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.40.tgz#37e2b0cf7c56026b4b21d3927cadf81adec32ac6" + dependencies: + "@babel/highlight" "7.0.0-beta.40" + +"@babel/generator@7.0.0-beta.40": + version "7.0.0-beta.40" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.0.0-beta.40.tgz#ab61f9556f4f71dbd1138949c795bb9a21e302ea" + dependencies: + "@babel/types" "7.0.0-beta.40" + jsesc "^2.5.1" + lodash "^4.2.0" + source-map "^0.5.0" + trim-right "^1.0.1" + +"@babel/helper-function-name@7.0.0-beta.40": + version "7.0.0-beta.40" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.40.tgz#9d033341ab16517f40d43a73f2d81fc431ccd7b6" + dependencies: + "@babel/helper-get-function-arity" "7.0.0-beta.40" + "@babel/template" "7.0.0-beta.40" + "@babel/types" "7.0.0-beta.40" + +"@babel/helper-get-function-arity@7.0.0-beta.40": + version "7.0.0-beta.40" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.40.tgz#ac0419cf067b0ec16453e1274f03878195791c6e" + dependencies: + "@babel/types" "7.0.0-beta.40" + +"@babel/highlight@7.0.0-beta.40": + version "7.0.0-beta.40" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.40.tgz#b43d67d76bf46e1d10d227f68cddcd263786b255" + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^3.0.0" + +"@babel/template@7.0.0-beta.40": + version "7.0.0-beta.40" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.40.tgz#034988c6424eb5c3268fe6a608626de1f4410fc8" + dependencies: + "@babel/code-frame" "7.0.0-beta.40" + "@babel/types" "7.0.0-beta.40" + babylon "7.0.0-beta.40" + lodash "^4.2.0" + +"@babel/traverse@^7.0.0-beta.40": + version "7.0.0-beta.40" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.40.tgz#d140e449b2e093ef9fe1a2eecc28421ffb4e521e" + dependencies: + "@babel/code-frame" "7.0.0-beta.40" + "@babel/generator" "7.0.0-beta.40" + "@babel/helper-function-name" "7.0.0-beta.40" + "@babel/types" "7.0.0-beta.40" + babylon "7.0.0-beta.40" + debug "^3.0.1" + globals "^11.1.0" + invariant "^2.2.0" + lodash "^4.2.0" + +"@babel/types@7.0.0-beta.40", "@babel/types@^7.0.0-beta.40": + version "7.0.0-beta.40" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.40.tgz#25c3d7aae14126abe05fcb098c65a66b6d6b8c14" + dependencies: + esutils "^2.0.2" + lodash "^4.2.0" + to-fast-properties "^2.0.0" + +"@types/node@*": + version "9.4.7" + resolved "http://registry.npmjs.org/@types/node/-/node-9.4.7.tgz#57d81cd98719df2c9de118f2d5f3b1120dcd7275" + +"@volst/eslint-config@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@volst/eslint-config/-/eslint-config-2.1.0.tgz#e2d7bbb6e74920ca816602e11bf84328b36dcd0c" + dependencies: + babel-eslint "^8.1.0" + eslint-plugin-import "^2.8.0" + eslint-plugin-react "^7.5.1" + +abbrev@^1.0.7: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + +acorn-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" + dependencies: + acorn "^3.0.4" + +acorn@^3.0.4: + version "3.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" + +acorn@^5.5.0: + version "5.5.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9" + +addons-linter@^0.39.1: + version "0.39.1" + resolved "https://registry.yarnpkg.com/addons-linter/-/addons-linter-0.39.1.tgz#f6c8ebf29eec1cfbc795632c6886c69613e6011b" + dependencies: + ajv "6.2.1" + ajv-merge-patch "3.0.0" + babel-register "6.26.0" + chalk "2.3.2" + cheerio "1.0.0-rc.2" + columnify "1.5.4" + common-tags "1.7.2" + crx-parser "0.1.2" + deepmerge "2.1.0" + dispensary "0.15.1" + doctoc "1.3.1" + es6-promisify "5.0.0" + eslint "4.18.2" + eslint-plugin-no-unsafe-innerhtml "1.0.16" + esprima "3.1.3" + first-chunk-stream "2.0.0" + fluent-syntax "^0.6.5" + glob "7.1.2" + is-mergeable-object "1.1.0" + jed "1.1.1" + os-locale "2.1.0" + pino "4.13.0" + po2json "0.4.5" + postcss "6.0.19" + probe-image-size "4.0.0" + relaxed-json "1.0.1" + semver "5.5.0" + shelljs "0.8.1" + snyk "^1.69.9" + source-map-support "0.5.3" + strip-bom-stream "3.0.0" + tosource "1.0.0" + upath "1.0.4" + whatwg-url "6.3.0" + xmldom "0.1.27" + yargs "11.0.0" + yauzl "2.9.1" + +ajv-keywords@^1.0.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" + +ajv-keywords@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" + +ajv-merge-patch@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ajv-merge-patch/-/ajv-merge-patch-3.0.0.tgz#76f071e391f419fe9fe3fea7e920a1ad824b2b61" + dependencies: + fast-json-patch "^1.0.0" + json-merge-patch "^0.2.3" + +ajv@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.2.1.tgz#28a6abc493a2abe0fb4c8507acaedb43fa550671" + dependencies: + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +ajv@^4.7.0: + version "4.11.8" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + +ajv@^5.1.0, ajv@^5.2.3, ajv@^5.3.0: + version "5.5.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +align-text@^0.1.1, align-text@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" + dependencies: + kind-of "^3.0.2" + longest "^1.0.1" + repeat-string "^1.5.2" + +anchor-markdown-header@^0.5.5: + version "0.5.7" + resolved "https://registry.yarnpkg.com/anchor-markdown-header/-/anchor-markdown-header-0.5.7.tgz#045063d76e6a1f9cd327a57a0126aa0fdec371a7" + dependencies: + emoji-regex "~6.1.0" + +ansi-escapes@^1.1.0, ansi-escapes@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" + +ansi-escapes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + dependencies: + color-convert "^1.9.0" + +ansi-styles@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178" + +ansicolors@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" + +any-promise@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + +archy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + dependencies: + sprintf-js "~1.0.2" + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + +array-from@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195" + +array-includes@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.7.0" + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + +arrify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + +asap@~2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + +asn1@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +async@~0.9.0: + version "0.9.2" + resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" + +async@~1.5: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + +async@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/async/-/async-2.0.1.tgz#b709cc0280a9c36f09f4536be823c838a9049e25" + dependencies: + lodash "^4.8.0" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + +aws4@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" + +babel-code-frame@^6.16.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + dependencies: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + +babel-core@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8" + dependencies: + babel-code-frame "^6.26.0" + babel-generator "^6.26.0" + babel-helpers "^6.24.1" + babel-messages "^6.23.0" + babel-register "^6.26.0" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + convert-source-map "^1.5.0" + debug "^2.6.8" + json5 "^0.5.1" + lodash "^4.17.4" + minimatch "^3.0.4" + path-is-absolute "^1.0.1" + private "^0.1.7" + slash "^1.0.0" + source-map "^0.5.6" + +babel-eslint@^8.1.0: + version "8.2.2" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.2.2.tgz#1102273354c6f0b29b4ea28a65f97d122296b68b" + dependencies: + "@babel/code-frame" "^7.0.0-beta.40" + "@babel/traverse" "^7.0.0-beta.40" + "@babel/types" "^7.0.0-beta.40" + babylon "^7.0.0-beta.40" + eslint-scope "~3.7.1" + eslint-visitor-keys "^1.0.0" + +babel-generator@^6.26.0: + version "6.26.1" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" + dependencies: + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + detect-indent "^4.0.0" + jsesc "^1.3.0" + lodash "^4.17.4" + source-map "^0.5.7" + trim-right "^1.0.1" + +babel-helpers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-messages@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" + dependencies: + babel-runtime "^6.22.0" + +babel-register@6.26.0, babel-register@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" + dependencies: + babel-core "^6.26.0" + babel-runtime "^6.26.0" + core-js "^2.5.0" + home-or-tmp "^2.0.0" + lodash "^4.17.4" + mkdirp "^0.5.1" + source-map-support "^0.4.15" + +babel-runtime@^6.22.0, babel-runtime@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + +babel-template@^6.24.1, babel-template@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" + dependencies: + babel-runtime "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + lodash "^4.17.4" + +babel-traverse@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" + dependencies: + babel-code-frame "^6.26.0" + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + debug "^2.6.8" + globals "^9.18.0" + invariant "^2.2.2" + lodash "^4.17.4" + +babel-types@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" + dependencies: + babel-runtime "^6.26.0" + esutils "^2.0.2" + lodash "^4.17.4" + to-fast-properties "^1.0.3" + +babylon@7.0.0-beta.40, babylon@^7.0.0-beta.40: + version "7.0.0-beta.40" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.40.tgz#91fc8cd56d5eb98b28e6fde41045f2957779940a" + +babylon@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" + +bail@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.2.tgz#f7d6c1731630a9f9f0d4d35ed1f962e2074a1764" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +base64-js@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.2.tgz#024f0f72afa25b75f9c0ee73cd4f55ec1bed9784" + +bcrypt-pbkdf@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" + dependencies: + tweetnacl "^0.14.3" + +boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + +boom@4.x.x: + version "4.3.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" + dependencies: + hoek "4.x.x" + +boom@5.x.x: + version "5.2.0" + resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" + dependencies: + hoek "4.x.x" + +bops@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/bops/-/bops-0.1.1.tgz#062e02a8daa801fa10f2e5dbe6740cff801fe17e" + dependencies: + base64-js "0.0.2" + to-utf8 "0.0.1" + +boundary@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/boundary/-/boundary-1.0.1.tgz#4d67dc2602c0cc16dd9bce7ebf87e948290f5812" + +boxen@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-0.3.1.tgz#a7d898243ae622f7abb6bb604d740a76c6a5461b" + dependencies: + chalk "^1.1.1" + filled-array "^1.0.0" + object-assign "^4.0.1" + repeating "^2.0.0" + string-width "^1.0.1" + widest-line "^1.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + +builtin-modules@^1.0.0, builtin-modules@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + +caller-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" + dependencies: + callsites "^0.2.0" + +callsites@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" + +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" + +camelcase@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + +camelcase@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" + +camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + +capture-stack-trace@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + +ccount@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.2.tgz#53b6a2f815bb77b9c2871f7b9a72c3a25f1d8e89" + +center-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" + dependencies: + align-text "^0.1.3" + lazy-cache "^1.0.3" + +chalk@2.3.2, chalk@^2.0.0, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.2.tgz#250dc96b07491bfd601e648d66ddf5f60c7a5c65" + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f" + dependencies: + ansi-styles "~1.0.0" + has-color "~0.1.0" + strip-ansi "~0.1.0" + +character-entities-html4@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-1.1.1.tgz#359a2a4a0f7e29d3dc2ac99bdbe21ee39438ea50" + +character-entities-legacy@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.1.tgz#f40779df1a101872bb510a3d295e1fccf147202f" + +character-entities@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.1.tgz#f76871be5ef66ddb7f8f8e3478ecc374c27d6dca" + +character-reference-invalid@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.1.tgz#942835f750e4ec61a308e60c2ef8cc1011202efc" + +chardet@^0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" + +cheerio@1.0.0-rc.2: + version "1.0.0-rc.2" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db" + dependencies: + css-select "~1.2.0" + dom-serializer "~0.1.0" + entities "~1.1.1" + htmlparser2 "^3.9.1" + lodash "^4.15.0" + parse5 "^3.0.1" + +chrome-webstore-upload-cli@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/chrome-webstore-upload-cli/-/chrome-webstore-upload-cli-1.1.1.tgz#3f8a3211c4d6f474ba3ca597e2bcb9aab227d44b" + dependencies: + chalk "^1.1.3" + chrome-webstore-upload "^0.2.0" + junk "^1.0.2" + meow "^3.7.0" + ora "^0.2.3" + pify "^2.3.0" + recursive-readdir "^2.0.0" + yazl "^2.3.1" + +chrome-webstore-upload@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/chrome-webstore-upload/-/chrome-webstore-upload-0.2.2.tgz#6293463a5d0475c8826ffd799a98357b04af4fcb" + dependencies: + got "^6.3.0" + +circular-json@^0.3.1: + version "0.3.3" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" + +cli-cursor@^1.0.1, cli-cursor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" + dependencies: + restore-cursor "^1.0.1" + +cli-cursor@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + dependencies: + restore-cursor "^2.0.0" + +cli-spinners@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c" + +cli-width@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" + +clite@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/clite/-/clite-0.3.0.tgz#e7fcbc8cc5bd3e7f8b84ed48db12e9474cc73441" + dependencies: + abbrev "^1.0.7" + debug "^2.2.0" + es6-promise "^3.1.2" + lodash.defaults "^4.0.1" + lodash.defaultsdeep "^4.3.1" + lodash.mergewith "^4.3.1" + then-fs "^2.0.0" + update-notifier "^0.6.0" + yargs "^4.3.2" + +cliui@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" + dependencies: + center-align "^0.1.1" + right-align "^0.1.1" + wordwrap "0.0.2" + +cliui@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + +cliui@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.0.0.tgz#743d4650e05f36d1ed2575b59638d87322bfbbcc" + dependencies: + string-width "^2.1.1" + strip-ansi "^4.0.0" + wrap-ansi "^2.0.0" + +clone-deep@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-0.3.0.tgz#348c61ae9cdbe0edfe053d91ff4cc521d790ede8" + dependencies: + for-own "^1.0.0" + is-plain-object "^2.0.1" + kind-of "^3.2.2" + shallow-clone "^0.1.2" + +clone@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.3.tgz#298d7e2231660f40c003c2ed3140decf3f53085f" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +collapse-white-space@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.3.tgz#4b906f670e5a963a87b76b0e1689643341b6023c" + +color-convert@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed" + dependencies: + color-name "^1.1.1" + +color-name@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + +columnify@1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.5.4.tgz#4737ddf1c7b69a8a7c340570782e947eec8e78bb" + dependencies: + strip-ansi "^3.0.0" + wcwidth "^1.0.0" + +combined-stream@1.0.6, combined-stream@~1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" + dependencies: + delayed-stream "~1.0.0" + +commander@^2.6.0: + version "2.15.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.0.tgz#ad2a23a1c3b036e392469b8012cec6b33b4c1322" + +common-tags@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.7.2.tgz#24d9768c63d253a56ecff93845b44b4df1d52771" + dependencies: + babel-runtime "^6.26.0" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +concat-stream@^1.5.2, concat-stream@^1.6.0: + version "1.6.1" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.1.tgz#261b8f518301f1d834e36342b9fea095d2620a26" + dependencies: + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +configstore@^1.0.0, configstore@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-1.4.0.tgz#c35781d0501d268c25c54b8b17f6240e8a4fb021" + dependencies: + graceful-fs "^4.1.2" + mkdirp "^0.5.0" + object-assign "^4.0.1" + os-tmpdir "^1.0.0" + osenv "^0.1.0" + uuid "^2.0.1" + write-file-atomic "^1.1.2" + xdg-basedir "^2.0.0" + +configstore@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-2.1.0.tgz#737a3a7036e9886102aa6099e47bb33ab1aba1a1" + dependencies: + dot-prop "^3.0.0" + graceful-fs "^4.1.2" + mkdirp "^0.5.0" + object-assign "^4.0.1" + os-tmpdir "^1.0.0" + osenv "^0.1.0" + uuid "^2.0.1" + write-file-atomic "^1.1.2" + xdg-basedir "^2.0.0" + +contains-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" + +convert-source-map@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5" + +core-js@^1.0.0: + version "1.2.7" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" + +core-js@^2.4.0, core-js@^2.5.0: + version "2.5.3" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e" + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +create-error-class@^3.0.0, create-error-class@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" + dependencies: + capture-stack-trace "^1.0.0" + +cross-spawn@^5.0.1, cross-spawn@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +crx-parser@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/crx-parser/-/crx-parser-0.1.2.tgz#7eeeed9eddc95e22c189382e34624044a89a5a6d" + +cryptiles@3.x.x: + version "3.1.2" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" + dependencies: + boom "5.x.x" + +css-select@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" + dependencies: + boolbase "~1.0.0" + css-what "2.1" + domutils "1.5.1" + nth-check "~1.0.1" + +css-what@2.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd" + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + dependencies: + array-find-index "^1.0.1" + +d@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" + dependencies: + es5-ext "^0.10.9" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +debug@2, debug@^2.1.1, debug@^2.1.2, debug@^2.1.3, debug@^2.2.0, debug@^2.6.8, debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + +debug@^3.0.1, debug@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + +decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + +deep-equal@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + +deep-extend@~0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + +deepmerge@2.1.0, deepmerge@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.1.0.tgz#511a54fff405fc346f0240bb270a3e9533a31102" + +defaults@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" + dependencies: + clone "^1.0.2" + +define-properties@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94" + dependencies: + foreach "^2.0.5" + object-keys "^1.0.8" + +del@^2.0.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" + dependencies: + globby "^5.0.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + rimraf "^2.2.8" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +detect-indent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" + dependencies: + repeating "^2.0.0" + +dispensary@0.15.1: + version "0.15.1" + resolved "https://registry.yarnpkg.com/dispensary/-/dispensary-0.15.1.tgz#1483a838de6c4fad516d2786d8a5ed4fe77cbb0d" + dependencies: + array-from "2.1.1" + async "~2.0.0" + natural-compare-lite "~1.4.0" + pino "~4.6.0" + request "~2.85.0" + semver "5.5.0" + sha.js "~2.4.4" + source-map-support "0.5.3" + yargs "~11.0.0" + +doctoc@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/doctoc/-/doctoc-1.3.1.tgz#f012e3603e3156254c2ef22ac88c7190f55426ba" + dependencies: + anchor-markdown-header "^0.5.5" + htmlparser2 "~3.9.2" + markdown-to-ast "~3.4.0" + minimist "~1.2.0" + underscore "~1.8.3" + update-section "^0.3.0" + +doctrine@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" + dependencies: + esutils "^2.0.2" + isarray "^1.0.0" + +doctrine@^2.0.0, doctrine@^2.0.2, doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + dependencies: + esutils "^2.0.2" + +dom-serializer@0, dom-serializer@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" + dependencies: + domelementtype "~1.1.1" + entities "~1.1.1" + +domelementtype@1, domelementtype@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2" + +domelementtype@~1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" + +domhandler@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.1.tgz#892e47000a99be55bbf3774ffea0561d8879c259" + dependencies: + domelementtype "1" + +domutils@1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" + dependencies: + dom-serializer "0" + domelementtype "1" + +domutils@^1.5.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + dependencies: + dom-serializer "0" + domelementtype "1" + +dot-prop@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-3.0.0.tgz#1b708af094a49c9a0e7dbcad790aba539dac1177" + dependencies: + is-obj "^1.0.0" + +duplexer2@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + dependencies: + readable-stream "^2.0.2" + +duplexer3@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" + +duplexify@^3.2.0: + version "3.5.4" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.4.tgz#4bb46c1796eabebeec4ca9a2e66b808cb7a3d8b4" + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" + dependencies: + jsbn "~0.1.0" + +email-validator@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/email-validator/-/email-validator-1.1.1.tgz#b07f3be7bac1dc099bc43e75f6ae399f552d5a80" + +emoji-regex@~6.1.0: + version "6.1.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.1.3.tgz#ec79a3969b02d2ecf2b72254279bf99bc7a83932" + +encoding@^0.1.11: + version "0.1.12" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" + dependencies: + iconv-lite "~0.4.13" + +end-of-stream@^1.0.0, end-of-stream@^1.1.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" + dependencies: + once "^1.4.0" + +entities@^1.1.1, entities@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" + +error-ex@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.7.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.10.0.tgz#1ecb36c197842a00d8ee4c2dfd8646bb97d60864" + dependencies: + es-to-primitive "^1.1.1" + function-bind "^1.1.1" + has "^1.0.1" + is-callable "^1.1.3" + is-regex "^1.0.4" + +es-to-primitive@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d" + dependencies: + is-callable "^1.1.1" + is-date-object "^1.0.1" + is-symbol "^1.0.1" + +es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14: + version "0.10.40" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.40.tgz#ab3d2179b943008c5e9ef241beb25ef41424c774" + dependencies: + es6-iterator "~2.0.3" + es6-symbol "~3.1.1" + +es6-iterator@^2.0.1, es6-iterator@~2.0.1, es6-iterator@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-map@^0.1.3: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-set "~0.1.5" + es6-symbol "~3.1.1" + event-emitter "~0.3.5" + +es6-promise@^3.0.2, es6-promise@^3.1.2: + version "3.3.1" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" + +es6-promise@^4.0.3, es6-promise@^4.1.1: + version "4.2.4" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29" + +es6-promisify@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + dependencies: + es6-promise "^4.0.3" + +es6-set@~0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-symbol "3.1.1" + event-emitter "~0.3.5" + +es6-symbol@3.1.1, es6-symbol@^3.1.1, es6-symbol@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" + dependencies: + d "1" + es5-ext "~0.10.14" + +es6-weak-map@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f" + dependencies: + d "1" + es5-ext "^0.10.14" + es6-iterator "^2.0.1" + es6-symbol "^3.1.1" + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +escope@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" + dependencies: + es6-map "^0.1.3" + es6-weak-map "^2.0.1" + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-import-resolver-node@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a" + dependencies: + debug "^2.6.9" + resolve "^1.5.0" + +eslint-module-utils@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.1.1.tgz#abaec824177613b8a95b299639e1b6facf473449" + dependencies: + debug "^2.6.8" + pkg-dir "^1.0.0" + +eslint-plugin-import@^2.8.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.9.0.tgz#26002efbfca5989b7288ac047508bd24f217b169" + dependencies: + builtin-modules "^1.1.1" + contains-path "^0.1.0" + debug "^2.6.8" + doctrine "1.5.0" + eslint-import-resolver-node "^0.3.1" + eslint-module-utils "^2.1.1" + has "^1.0.1" + lodash "^4.17.4" + minimatch "^3.0.3" + read-pkg-up "^2.0.0" + +eslint-plugin-no-unsafe-innerhtml@1.0.16: + version "1.0.16" + resolved "https://registry.yarnpkg.com/eslint-plugin-no-unsafe-innerhtml/-/eslint-plugin-no-unsafe-innerhtml-1.0.16.tgz#7d02878c8e9bf7916b88836d5ac122b42f151932" + dependencies: + eslint "^3.7.1" + +eslint-plugin-react@^7.5.1: + version "7.7.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.7.0.tgz#f606c719dbd8a1a2b3d25c16299813878cca0160" + dependencies: + doctrine "^2.0.2" + has "^1.0.1" + jsx-ast-utils "^2.0.1" + prop-types "^15.6.0" + +eslint-scope@^3.7.1, eslint-scope@~3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8" + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-visitor-keys@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" + +eslint@4.18.2, eslint@^4.18.2: + version "4.18.2" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.18.2.tgz#0f81267ad1012e7d2051e186a9004cc2267b8d45" + dependencies: + ajv "^5.3.0" + babel-code-frame "^6.22.0" + chalk "^2.1.0" + concat-stream "^1.6.0" + cross-spawn "^5.1.0" + debug "^3.1.0" + doctrine "^2.1.0" + eslint-scope "^3.7.1" + eslint-visitor-keys "^1.0.0" + espree "^3.5.2" + esquery "^1.0.0" + esutils "^2.0.2" + file-entry-cache "^2.0.0" + functional-red-black-tree "^1.0.1" + glob "^7.1.2" + globals "^11.0.1" + ignore "^3.3.3" + imurmurhash "^0.1.4" + inquirer "^3.0.6" + is-resolvable "^1.0.0" + js-yaml "^3.9.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.3.0" + lodash "^4.17.4" + minimatch "^3.0.2" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + optionator "^0.8.2" + path-is-inside "^1.0.2" + pluralize "^7.0.0" + progress "^2.0.0" + require-uncached "^1.0.3" + semver "^5.3.0" + strip-ansi "^4.0.0" + strip-json-comments "~2.0.1" + table "4.0.2" + text-table "~0.2.0" + +eslint@^3.7.1: + version "3.19.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.19.0.tgz#c8fc6201c7f40dd08941b87c085767386a679acc" + dependencies: + babel-code-frame "^6.16.0" + chalk "^1.1.3" + concat-stream "^1.5.2" + debug "^2.1.1" + doctrine "^2.0.0" + escope "^3.6.0" + espree "^3.4.0" + esquery "^1.0.0" + estraverse "^4.2.0" + esutils "^2.0.2" + file-entry-cache "^2.0.0" + glob "^7.0.3" + globals "^9.14.0" + ignore "^3.2.0" + imurmurhash "^0.1.4" + inquirer "^0.12.0" + is-my-json-valid "^2.10.0" + is-resolvable "^1.0.0" + js-yaml "^3.5.1" + json-stable-stringify "^1.0.0" + levn "^0.3.0" + lodash "^4.0.0" + mkdirp "^0.5.0" + natural-compare "^1.4.0" + optionator "^0.8.2" + path-is-inside "^1.0.1" + pluralize "^1.2.1" + progress "^1.1.8" + require-uncached "^1.0.2" + shelljs "^0.7.5" + strip-bom "^3.0.0" + strip-json-comments "~2.0.1" + table "^3.7.8" + text-table "~0.2.0" + user-home "^2.0.0" + +espree@^3.4.0, espree@^3.5.2: + version "3.5.4" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7" + dependencies: + acorn "^5.5.0" + acorn-jsx "^3.0.0" + +esprima@3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" + +esprima@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" + +esquery@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa" + dependencies: + estraverse "^4.0.0" + +esrecurse@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" + dependencies: + estraverse "^4.1.0" + +estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +event-emitter@~0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + dependencies: + d "1" + es5-ext "~0.10.14" + +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +exit-hook@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" + +extend@^3.0.0, extend@~3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" + +external-editor@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.1.0.tgz#3d026a21b7f95b5726387d4200ac160d372c3b48" + dependencies: + chardet "^0.4.0" + iconv-lite "^0.4.17" + tmp "^0.0.33" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + +fast-deep-equal@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" + +fast-json-parse@^1.0.0, fast-json-parse@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/fast-json-parse/-/fast-json-parse-1.0.3.tgz#43e5c61ee4efa9265633046b770fb682a7577c4d" + +fast-json-patch@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/fast-json-patch/-/fast-json-patch-1.2.2.tgz#d377d97c6911dbdd2a1c80bfacda048a4f83bbf9" + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + +fast-levenshtein@~2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + +fast-safe-stringify@^1.0.8, fast-safe-stringify@^1.1.11, fast-safe-stringify@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-1.2.3.tgz#9fe22c37fb2f7f86f06b8f004377dbf8f1ee7bc1" + +fbjs@^0.8.16: + version "0.8.16" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db" + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.9" + +fd-slicer@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" + dependencies: + pend "~1.2.0" + +figures@^1.3.5: + version "1.7.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + +figures@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" + dependencies: + flat-cache "^1.2.1" + object-assign "^4.0.1" + +filled-array@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/filled-array/-/filled-array-1.1.0.tgz#c3c4f6c663b923459a9aa29912d2d031f1507f84" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + dependencies: + locate-path "^2.0.0" + +first-chunk-stream@2.0.0, first-chunk-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz#1bdecdb8e083c0664b91945581577a43a9f31d70" + dependencies: + readable-stream "^2.0.2" + +flat-cache@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.0.tgz#d3030b32b38154f4e3b7e9c709f490f7ef97c481" + dependencies: + circular-json "^0.3.1" + del "^2.0.2" + graceful-fs "^4.1.2" + write "^0.2.1" + +flatstr@^1.0.4, flatstr@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.5.tgz#5b451b08cbd48e2eac54a2bbe0bf46165aa14be3" + +fluent-syntax@^0.6.5: + version "0.6.5" + resolved "https://registry.yarnpkg.com/fluent-syntax/-/fluent-syntax-0.6.5.tgz#e40a76fa41ce55ba6b8ab29d63b0e4c5e5b01e99" + +for-in@^0.1.3: + version "0.1.8" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" + +for-in@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + +for-own@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" + dependencies: + for-in "^1.0.1" + +foreach@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" + dependencies: + asynckit "^0.4.0" + combined-stream "1.0.6" + mime-types "^2.1.12" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +function-bind@^1.0.2, function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + +generate-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74" + +generate-object-property@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" + dependencies: + is-property "^1.0.0" + +get-caller-file@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" + +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + dependencies: + assert-plus "^1.0.0" + +gettext-parser@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/gettext-parser/-/gettext-parser-1.1.0.tgz#2c5a6638d893934b9b55037d0ad82cb7004b2679" + dependencies: + encoding "^0.1.11" + +glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.0.1, globals@^11.1.0: + version "11.3.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.3.0.tgz#e04fdb7b9796d8adac9c8f64c14837b2313378b0" + +globals@^9.14.0, globals@^9.18.0: + version "9.18.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" + +globby@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" + dependencies: + array-union "^1.0.1" + arrify "^1.0.0" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +got@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/got/-/got-3.3.1.tgz#e5d0ed4af55fc3eef4d56007769d98192bcb2eca" + dependencies: + duplexify "^3.2.0" + infinity-agent "^2.0.0" + is-redirect "^1.0.0" + is-stream "^1.0.0" + lowercase-keys "^1.0.0" + nested-error-stacks "^1.0.0" + object-assign "^3.0.0" + prepend-http "^1.0.0" + read-all-stream "^3.0.0" + timed-out "^2.0.0" + +got@^5.0.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/got/-/got-5.7.1.tgz#5f81635a61e4a6589f180569ea4e381680a51f35" + dependencies: + create-error-class "^3.0.1" + duplexer2 "^0.1.4" + is-redirect "^1.0.0" + is-retry-allowed "^1.0.0" + is-stream "^1.0.0" + lowercase-keys "^1.0.0" + node-status-codes "^1.0.0" + object-assign "^4.0.1" + parse-json "^2.1.0" + pinkie-promise "^2.0.0" + read-all-stream "^3.0.0" + readable-stream "^2.0.5" + timed-out "^3.0.0" + unzip-response "^1.0.2" + url-parse-lax "^1.0.0" + +got@^6.3.0: + version "6.7.1" + resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" + dependencies: + create-error-class "^3.0.0" + duplexer3 "^0.1.4" + get-stream "^3.0.0" + is-redirect "^1.0.0" + is-retry-allowed "^1.0.0" + is-stream "^1.0.0" + lowercase-keys "^1.0.0" + safe-buffer "^5.0.1" + timed-out "^4.0.0" + unzip-response "^2.0.1" + url-parse-lax "^1.0.0" + +graceful-fs@^4.1.11, graceful-fs@^4.1.2: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +graphlib@^2.1.1: + version "2.1.5" + resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.5.tgz#6afe1afcc5148555ec799e499056795bd6938c87" + dependencies: + lodash "^4.11.1" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + +har-validator@~5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" + dependencies: + ajv "^5.1.0" + har-schema "^2.0.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-color@~0.1.0: + version "0.1.7" + resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + +has@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" + dependencies: + function-bind "^1.0.2" + +hasbin@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/hasbin/-/hasbin-1.2.3.tgz#78c5926893c80215c2b568ae1fd3fcab7a2696b0" + dependencies: + async "~1.5" + +hawk@~6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" + dependencies: + boom "4.x.x" + cryptiles "3.x.x" + hoek "4.x.x" + sntp "2.x.x" + +hoek@4.x.x: + version "4.2.1" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" + +home-or-tmp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.1" + +hosted-git-info@^2.1.4: + version "2.6.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.6.0.tgz#23235b29ab230c576aab0d4f13fc046b0b038222" + +htmlparser2@^3.9.1, htmlparser2@~3.9.2: + version "3.9.2" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338" + dependencies: + domelementtype "^1.3.0" + domhandler "^2.3.0" + domutils "^1.5.1" + entities "^1.1.1" + inherits "^2.0.1" + readable-stream "^2.0.2" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +iconv-lite@^0.4.17, iconv-lite@^0.4.4, iconv-lite@~0.4.13: + version "0.4.19" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" + +ignore@^3.2.0, ignore@^3.3.3: + version "3.3.7" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + dependencies: + repeating "^2.0.0" + +infinity-agent@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/infinity-agent/-/infinity-agent-2.0.3.tgz#45e0e2ff7a9eb030b27d62b74b3744b7a7ac4216" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +ini@1.x.x, ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + +inquirer@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-1.0.3.tgz#ebe3a0948571bcc46ccccbe2f9bcec251e984bd0" + dependencies: + ansi-escapes "^1.1.0" + chalk "^1.0.0" + cli-cursor "^1.0.1" + cli-width "^2.0.0" + figures "^1.3.5" + lodash "^4.3.0" + mute-stream "0.0.6" + pinkie-promise "^2.0.0" + run-async "^2.2.0" + rx "^4.1.0" + string-width "^1.0.1" + strip-ansi "^3.0.0" + through "^2.3.6" + +inquirer@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" + dependencies: + ansi-escapes "^1.1.0" + ansi-regex "^2.0.0" + chalk "^1.0.0" + cli-cursor "^1.0.1" + cli-width "^2.0.0" + figures "^1.3.5" + lodash "^4.3.0" + readline2 "^1.0.1" + run-async "^0.1.0" + rx-lite "^3.1.2" + string-width "^1.0.1" + strip-ansi "^3.0.0" + through "^2.3.6" + +inquirer@^3.0.6: + version "3.3.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" + dependencies: + ansi-escapes "^3.0.0" + chalk "^2.0.0" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^2.0.4" + figures "^2.0.0" + lodash "^4.3.0" + mute-stream "0.0.7" + run-async "^2.2.0" + rx-lite "^4.0.8" + rx-lite-aggregates "^4.0.8" + string-width "^2.1.0" + strip-ansi "^4.0.0" + through "^2.3.6" + +interpret@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" + +invariant@^2.2.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.3.tgz#1a827dfde7dcbd7c323f0ca826be8fa7c5e9d688" + dependencies: + loose-envify "^1.0.0" + +invariant@^2.2.2: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + dependencies: + loose-envify "^1.0.0" + +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + +is-alphabetical@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.1.tgz#c77079cc91d4efac775be1034bf2d243f95e6f08" + +is-alphanumerical@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.1.tgz#dfb4aa4d1085e33bdb61c2dee9c80e9c6c19f53b" + dependencies: + is-alphabetical "^1.0.0" + is-decimal "^1.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + +is-buffer@^1.0.2, is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + dependencies: + builtin-modules "^1.0.0" + +is-callable@^1.1.1, is-callable@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2" + +is-date-object@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" + +is-decimal@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.1.tgz#f5fb6a94996ad9e8e3761fbfbd091f1fca8c4e82" + +is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-hexadecimal@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.1.tgz#6e084bbc92061fbb0971ec58b6ce6d404e24da69" + +is-mergeable-object@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-mergeable-object/-/is-mergeable-object-1.1.0.tgz#a846e8cf0e2bad6a8cf8b243b63b4c43b9907990" + +is-my-ip-valid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824" + +is-my-json-valid@^2.10.0: + version "2.17.2" + resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz#6b2103a288e94ef3de5cf15d29dd85fc4b78d65c" + dependencies: + generate-function "^2.0.0" + generate-object-property "^1.1.0" + is-my-ip-valid "^1.0.0" + jsonpointer "^4.0.0" + xtend "^4.0.0" + +is-npm@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" + +is-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + +is-path-in-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" + dependencies: + is-path-inside "^1.0.0" + +is-path-inside@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" + dependencies: + path-is-inside "^1.0.1" + +is-plain-object@^2.0.1: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + dependencies: + isobject "^3.0.1" + +is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + +is-property@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + +is-redirect@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" + +is-regex@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" + dependencies: + has "^1.0.1" + +is-resolvable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" + +is-retry-allowed@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34" + +is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +is-symbol@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +is-utf8@^0.2.0, is-utf8@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + +isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + +isomorphic-fetch@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" + dependencies: + node-fetch "^1.0.1" + whatwg-fetch ">=0.10.0" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +jed@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/jed/-/jed-1.1.1.tgz#7a549bbd9ffe1585b0cd0a191e203055bee574b4" + +js-tokens@^3.0.0, js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + +js-yaml@^3.5.1, js-yaml@^3.5.3, js-yaml@^3.9.1: + version "3.11.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef" + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + +jsesc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" + +jsesc@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.1.tgz#e421a2a8e20d6b0819df28908f782526b96dd1fe" + +json-merge-patch@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-merge-patch/-/json-merge-patch-0.2.3.tgz#fa2c6b5af87da77bae2966a589d52e23ed81fe40" + dependencies: + deep-equal "^1.0.0" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + +json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + dependencies: + jsonify "~0.0.0" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +json5@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + +jsonpointer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +jsx-ast-utils@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz#e801b1b39985e20fffc87b40e3748080e2dcac7f" + dependencies: + array-includes "^3.0.3" + +junk@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/junk/-/junk-1.0.3.tgz#87be63488649cbdca6f53ab39bec9ccd2347f592" + +kind-of@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5" + dependencies: + is-buffer "^1.0.2" + +kind-of@^3.0.2, kind-of@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + dependencies: + is-buffer "^1.1.5" + +latest-version@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-1.0.1.tgz#72cfc46e3e8d1be651e1ebb54ea9f6ea96f374bb" + dependencies: + package-json "^1.0.0" + +latest-version@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-2.0.0.tgz#56f8d6139620847b8017f8f1f4d78e211324168b" + dependencies: + package-json "^2.0.0" + +lazy-cache@^0.2.3: + version "0.2.7" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-0.2.7.tgz#7feddf2dcb6edb77d11ef1d117ab5ffdf0ab1b65" + +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" + +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + dependencies: + invert-kv "^1.0.0" + +levn@^0.3.0, levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +lodash.assign@^4.0.3, lodash.assign@^4.0.6: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" + +lodash.clonedeep@^4.3.0, lodash.clonedeep@^4.3.1: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + +lodash.defaults@^4.0.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + +lodash.defaultsdeep@^4.3.1: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.0.tgz#bec1024f85b1bd96cbea405b23c14ad6443a6f81" + +lodash.mergewith@^4.3.1: + version "4.6.1" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" + +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + +lodash@^4.0.0, lodash@^4.11.1, lodash@^4.15.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.8.0: + version "4.17.5" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" + +longest-streak@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-1.0.0.tgz#d06597c4d4c31b52ccb1f5d8f8fe7148eafd6965" + +longest@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" + +loose-envify@^1.0.0, loose-envify@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" + dependencies: + js-tokens "^3.0.0" + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +lowercase-keys@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" + +lru-cache@^4.0.0, lru-cache@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.2.tgz#45234b2e6e2f2b33da125624c4664929a0224c3f" + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + +markdown-table@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-0.4.0.tgz#890c2c1b3bfe83fb00e4129b8e4cfe645270f9d1" + +markdown-to-ast@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/markdown-to-ast/-/markdown-to-ast-3.4.0.tgz#0e2cba81390b0549a9153ec3b0d915b61c164be7" + dependencies: + debug "^2.1.3" + remark "^5.0.1" + structured-source "^3.0.2" + traverse "^0.6.6" + +mem@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" + dependencies: + mimic-fn "^1.0.0" + +meow@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + +mime-db@~1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" + +mime-types@^2.1.12, mime-types@~2.1.17: + version "2.1.18" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" + dependencies: + mime-db "~1.33.0" + +mimic-fn@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" + +minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0, minimist@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +mixin-object@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e" + dependencies: + for-in "^0.1.3" + is-extendable "^0.1.1" + +mkdirp@^0.5.0, mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +mute-stream@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" + +mute-stream@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.6.tgz#48962b19e169fd1dfc240b3f1e7317627bbc47db" + +mute-stream@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" + +natural-compare-lite@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + +nconf@^0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/nconf/-/nconf-0.7.2.tgz#a05fdf22dc01c378dd5c4df27f2dc90b9aa8bb00" + dependencies: + async "~0.9.0" + ini "1.x.x" + yargs "~3.15.0" + +needle@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.0.tgz#f14efc69cee1024b72c8b21c7bdf94a731dc12fa" + dependencies: + debug "^2.1.2" + iconv-lite "^0.4.4" + sax "^1.2.4" + +nested-error-stacks@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-1.0.2.tgz#19f619591519f096769a5ba9a86e6eeec823c3cf" + dependencies: + inherits "~2.0.1" + +next-tick@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" + +node-fetch@^1.0.1: + version "1.7.3" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + +node-status-codes@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-status-codes/-/node-status-codes-1.0.0.tgz#5ae5541d024645d32a58fcddc9ceecea7ae3ac2f" + +nomnom@1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7" + dependencies: + chalk "~0.4.0" + underscore "~1.6.0" + +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: + version "2.4.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + dependencies: + path-key "^2.0.0" + +nth-check@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.1.tgz#9929acdf628fc2c41098deab82ac580cf149aae4" + dependencies: + boolbase "~1.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +oauth-sign@~0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + +object-assign@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" + +object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +object-keys@^1.0.8: + version "1.0.11" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" + +once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +onetime@^1.0.0: + version "1.1.0" + resolved "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + +onetime@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + dependencies: + mimic-fn "^1.0.0" + +open@^0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/open/-/open-0.0.5.tgz#42c3e18ec95466b6bf0dc42f3a2945c3f0cad8fc" + +optionator@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.4" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + wordwrap "~1.0.0" + +ora@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4" + dependencies: + chalk "^1.1.1" + cli-cursor "^1.0.2" + cli-spinners "^0.1.2" + object-assign "^4.0.1" + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-locale@2.1.0, os-locale@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" + dependencies: + execa "^0.7.0" + lcid "^1.0.0" + mem "^1.1.0" + +os-locale@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" + dependencies: + lcid "^1.0.0" + +os-name@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/os-name/-/os-name-1.0.3.tgz#1b379f64835af7c5a7f498b357cb95215c159edf" + dependencies: + osx-release "^1.0.0" + win-release "^1.0.0" + +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +osenv@^0.1.0: + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +osx-release@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/osx-release/-/osx-release-1.1.0.tgz#f217911a28136949af1bf9308b241e2737d3cd6c" + dependencies: + minimist "^1.1.0" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + +p-limit@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c" + dependencies: + p-try "^1.0.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + dependencies: + p-limit "^1.1.0" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + +package-json@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-1.2.0.tgz#c8ecac094227cdf76a316874ed05e27cc939a0e0" + dependencies: + got "^3.2.0" + registry-url "^3.0.0" + +package-json@^2.0.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-2.4.0.tgz#0d15bd67d1cbbddbb2ca222ff2edb86bcb31a8bb" + dependencies: + got "^5.0.0" + registry-auth-token "^3.0.1" + registry-url "^3.0.3" + semver "^5.1.0" + +parse-entities@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.1.1.tgz#8112d88471319f27abae4d64964b122fe4e1b890" + dependencies: + character-entities "^1.0.0" + character-entities-legacy "^1.0.0" + character-reference-invalid "^1.0.0" + is-alphanumerical "^1.0.0" + is-decimal "^1.0.0" + is-hexadecimal "^1.0.0" + +parse-json@^2.1.0, parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + dependencies: + error-ex "^1.2.0" + +parse5@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c" + dependencies: + "@types/node" "*" + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + dependencies: + pinkie-promise "^2.0.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + +path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-is-inside@^1.0.1, path-is-inside@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + +path-key@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + +path-parse@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + dependencies: + pify "^2.0.0" + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + +pify@^2.0.0, pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +pino-std-serializers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-1.0.0.tgz#7607ab8c5b7c32bf7c4dada149da4ce3c14a3b75" + +pino@4.13.0: + version "4.13.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-4.13.0.tgz#2defbc3ef9868f27d59767d44164824867516114" + dependencies: + chalk "^2.3.0" + fast-json-parse "^1.0.3" + fast-safe-stringify "^1.2.3" + flatstr "^1.0.5" + pino-std-serializers "^1.0.0" + pump "^2.0.1" + quick-format-unescaped "^1.1.2" + split2 "^2.2.0" + +pino@~4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-4.6.0.tgz#68e78ee9f3799a208a120a4533207943f38f6c5d" + dependencies: + chalk "^1.1.1" + fast-json-parse "^1.0.0" + fast-safe-stringify "^1.1.11" + flatstr "^1.0.4" + pump "^1.0.2" + quick-format-unescaped "^1.1.1" + split2 "^2.0.1" + +pkg-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4" + dependencies: + find-up "^1.0.0" + +pluralize@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" + +pluralize@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" + +po2json@0.4.5: + version "0.4.5" + resolved "https://registry.yarnpkg.com/po2json/-/po2json-0.4.5.tgz#47bb2952da32d58a1be2f256a598eebc0b745118" + dependencies: + gettext-parser "1.1.0" + nomnom "1.8.1" + +postcss@6.0.19: + version "6.0.19" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.19.tgz#76a78386f670b9d9494a655bf23ac012effd1555" + dependencies: + chalk "^2.3.1" + source-map "^0.6.1" + supports-color "^5.2.0" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + +prepend-http@^1.0.0, prepend-http@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + +private@^0.1.7: + version "0.1.8" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" + +probe-image-size@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/probe-image-size/-/probe-image-size-4.0.0.tgz#d35b71759e834bcf580ea9f18ec8b9265c0977eb" + dependencies: + any-promise "^1.3.0" + deepmerge "^2.0.1" + inherits "^2.0.3" + next-tick "^1.0.0" + request "^2.83.0" + stream-parser "~0.3.1" + +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + +progress@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" + +progress@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" + +"promise@>=3.2 <8", promise@^7.1.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + dependencies: + asap "~2.0.3" + +prop-types@^15.6.0: + version "15.6.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca" + dependencies: + fbjs "^0.8.16" + loose-envify "^1.3.1" + object-assign "^4.1.1" + +proxy-from-env@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + +pump@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954" + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pump@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +punycode@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d" + +qs@~6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + +quick-format-unescaped@^1.1.1, quick-format-unescaped@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-1.1.2.tgz#0ca581de3174becef25ac3c2e8956342381db698" + dependencies: + fast-safe-stringify "^1.0.8" + +rc@^1.0.1, rc@^1.1.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.6.tgz#eb18989c6d4f4f162c399f79ddd29f3835568092" + dependencies: + deep-extend "~0.4.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +read-all-stream@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/read-all-stream/-/read-all-stream-3.1.0.tgz#35c3e177f2078ef789ee4bfafa4373074eaef4fa" + dependencies: + pinkie-promise "^2.0.0" + readable-stream "^2.0.0" + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + +readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.2.2: + version "2.3.5" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.5.tgz#b4f85003a938cbb6ecbce2a124fb1012bd1a838d" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.0.3" + util-deprecate "~1.0.1" + +readline2@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + mute-stream "0.0.5" + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + dependencies: + resolve "^1.1.6" + +recursive-readdir@^2.0.0, recursive-readdir@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" + dependencies: + minimatch "3.0.4" + +redent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + dependencies: + indent-string "^2.1.0" + strip-indent "^1.0.1" + +regenerator-runtime@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" + +registry-auth-token@^3.0.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.3.2.tgz#851fd49038eecb586911115af845260eec983f20" + dependencies: + rc "^1.1.6" + safe-buffer "^5.0.1" + +registry-url@^3.0.0, registry-url@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942" + dependencies: + rc "^1.0.1" + +relaxed-json@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/relaxed-json/-/relaxed-json-1.0.1.tgz#7c8d4aa2f095704cd020e32e8099bcae103f0bd4" + dependencies: + chalk "^1.0.0" + commander "^2.6.0" + +remark-parse@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-1.1.0.tgz#c3ca10f9a8da04615c28f09aa4e304510526ec21" + dependencies: + collapse-white-space "^1.0.0" + extend "^3.0.0" + parse-entities "^1.0.2" + repeat-string "^1.5.4" + trim "0.0.1" + trim-trailing-lines "^1.0.0" + unherit "^1.0.4" + unist-util-remove-position "^1.0.0" + vfile-location "^2.0.0" + +remark-stringify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-1.1.0.tgz#a7105e25b9ee2bf9a49b75d2c423f11b06ae2092" + dependencies: + ccount "^1.0.0" + extend "^3.0.0" + longest-streak "^1.0.0" + markdown-table "^0.4.0" + parse-entities "^1.0.2" + repeat-string "^1.5.4" + stringify-entities "^1.0.1" + unherit "^1.0.4" + +remark@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/remark/-/remark-5.1.0.tgz#cb463bd3dbcb4b99794935eee1cf71d7a8e3068c" + dependencies: + remark-parse "^1.1.0" + remark-stringify "^1.1.0" + unified "^4.1.1" + +repeat-string@^1.5.2, repeat-string@^1.5.4: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + +repeating@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-1.1.3.tgz#3d4114218877537494f97f77f9785fab810fa4ac" + dependencies: + is-finite "^1.0.0" + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + dependencies: + is-finite "^1.0.0" + +request@^2.83.0, request@~2.85.0: + version "2.85.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.6.0" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.1" + forever-agent "~0.6.1" + form-data "~2.3.1" + har-validator "~5.0.3" + hawk "~6.0.2" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.17" + oauth-sign "~0.8.2" + performance-now "^2.1.0" + qs "~6.5.1" + safe-buffer "^5.1.1" + stringstream "~0.0.5" + tough-cookie "~2.3.3" + tunnel-agent "^0.6.0" + uuid "^3.1.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + +require-uncached@^1.0.2, require-uncached@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" + dependencies: + caller-path "^0.1.0" + resolve-from "^1.0.0" + +resolve-from@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" + +resolve@^1.1.6, resolve@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36" + dependencies: + path-parse "^1.0.5" + +restore-cursor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" + dependencies: + exit-hook "^1.0.0" + onetime "^1.0.0" + +restore-cursor@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + dependencies: + onetime "^2.0.0" + signal-exit "^3.0.2" + +right-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" + dependencies: + align-text "^0.1.1" + +rimraf@^2.2.8: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + dependencies: + glob "^7.0.5" + +run-async@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" + dependencies: + once "^1.3.0" + +run-async@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" + dependencies: + is-promise "^2.1.0" + +rx-lite-aggregates@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be" + dependencies: + rx-lite "*" + +rx-lite@*, rx-lite@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" + +rx-lite@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" + +rx@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" + +safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + +sax@>=0.6.0, sax@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + +semver-diff@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" + dependencies: + semver "^5.0.3" + +"semver@2 || 3 || 4 || 5", semver@5.5.0, semver@^5.0.1, semver@^5.0.3, semver@^5.1.0, semver@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + +sha.js@~2.4.4: + version "2.4.10" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.10.tgz#b1fde5cd7d11a5626638a07c604ab909cfa31f9b" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +shallow-clone@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-0.1.2.tgz#5909e874ba77106d73ac414cfec1ffca87d97060" + dependencies: + is-extendable "^0.1.1" + kind-of "^2.0.1" + lazy-cache "^0.2.3" + mixin-object "^2.0.1" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + +shelljs@0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.1.tgz#729e038c413a2254c4078b95ed46e0397154a9f1" + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +shelljs@^0.7.5: + version "0.7.8" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3" + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + +slice-ansi@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" + +slice-ansi@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d" + dependencies: + is-fullwidth-code-point "^2.0.0" + +slide@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" + +sntp@2.x.x: + version "2.1.0" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" + dependencies: + hoek "4.x.x" + +snyk-config@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/snyk-config/-/snyk-config-1.0.1.tgz#f27aec2498b24027ac719214026521591111508f" + dependencies: + debug "^2.2.0" + nconf "^0.7.2" + path-is-absolute "^1.0.0" + +snyk-go-plugin@1.4.5: + version "1.4.5" + resolved "https://registry.yarnpkg.com/snyk-go-plugin/-/snyk-go-plugin-1.4.5.tgz#bf462656caade0603970b68e756f4b389c3aeaaa" + dependencies: + graphlib "^2.1.1" + toml "^2.3.2" + +snyk-gradle-plugin@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/snyk-gradle-plugin/-/snyk-gradle-plugin-1.2.0.tgz#ef5aea5d132905cbf0315c72d9d96b24aa4a75dd" + dependencies: + clone-deep "^0.3.0" + +snyk-module@1.8.1, snyk-module@^1.6.0, snyk-module@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/snyk-module/-/snyk-module-1.8.1.tgz#31d5080fb1c0dfd6fa8567dd34a523fd02bf1fca" + dependencies: + debug "^2.2.0" + hosted-git-info "^2.1.4" + +snyk-mvn-plugin@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/snyk-mvn-plugin/-/snyk-mvn-plugin-1.1.1.tgz#15c13131a368dde487763de93557ad5fb9572ffe" + +snyk-nuget-plugin@1.3.9: + version "1.3.9" + resolved "https://registry.yarnpkg.com/snyk-nuget-plugin/-/snyk-nuget-plugin-1.3.9.tgz#bcdc503eafe9f3eeb4024b756ded4d0c3b265d12" + dependencies: + debug "^3.1.0" + es6-promise "^4.1.1" + xml2js "^0.4.17" + zip "^1.2.0" + +snyk-php-plugin@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/snyk-php-plugin/-/snyk-php-plugin-1.3.2.tgz#51c19171dee0cd35158a7aa835fe02a97dc84ab8" + dependencies: + debug "^3.1.0" + +snyk-policy@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/snyk-policy/-/snyk-policy-1.10.2.tgz#2a7bf0f07c7b811b9dda93cf9bbb10dc992dd7bc" + dependencies: + debug "^2.2.0" + email-validator "^1.1.1" + es6-promise "^3.1.2" + js-yaml "^3.5.3" + lodash.clonedeep "^4.3.1" + semver "^5.1.0" + snyk-module "^1.8.1" + snyk-resolve "^1.0.0" + snyk-try-require "^1.1.1" + then-fs "^2.0.0" + +snyk-python-plugin@1.5.7: + version "1.5.7" + resolved "https://registry.yarnpkg.com/snyk-python-plugin/-/snyk-python-plugin-1.5.7.tgz#fe45da46b59becec6e41f34023948246778ebc3e" + +snyk-resolve-deps@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/snyk-resolve-deps/-/snyk-resolve-deps-1.7.0.tgz#13743a058437dff890baaf437c333c966a743cb6" + dependencies: + abbrev "^1.0.7" + ansicolors "^0.3.2" + clite "^0.3.0" + debug "^2.2.0" + es6-promise "^3.0.2" + lodash "^4.0.0" + lru-cache "^4.0.0" + minimist "^1.2.0" + semver "^5.1.0" + snyk-module "^1.6.0" + snyk-resolve "^1.0.0" + snyk-tree "^1.0.0" + snyk-try-require "^1.1.1" + then-fs "^2.0.0" + +snyk-resolve@1.0.0, snyk-resolve@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/snyk-resolve/-/snyk-resolve-1.0.0.tgz#bbe9196d37f57c39251e6be75ccdd5b2097e99a2" + dependencies: + debug "^2.2.0" + then-fs "^2.0.0" + +snyk-sbt-plugin@1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/snyk-sbt-plugin/-/snyk-sbt-plugin-1.2.5.tgz#e86a3b4e727d206f7e41154b0dd2019b16102360" + dependencies: + debug "^2.2.0" + +snyk-tree@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/snyk-tree/-/snyk-tree-1.0.0.tgz#0fb73176dbf32e782f19100294160448f9111cc8" + dependencies: + archy "^1.0.0" + +snyk-try-require@^1.1.1, snyk-try-require@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/snyk-try-require/-/snyk-try-require-1.2.0.tgz#30fc2b11c07064591ee35780c826be91312f2144" + dependencies: + debug "^2.2.0" + es6-promise "^3.1.2" + lodash.clonedeep "^4.3.0" + lru-cache "^4.0.0" + then-fs "^2.0.0" + +snyk@^1.69.9: + version "1.70.2" + resolved "https://registry.yarnpkg.com/snyk/-/snyk-1.70.2.tgz#5acaf01d882a2719d0ec277248499a36dc4d0487" + dependencies: + abbrev "^1.0.7" + ansi-escapes "^1.3.0" + chalk "^1.1.1" + configstore "^1.2.0" + debug "^3.1.0" + es6-promise "^3.0.2" + hasbin "^1.2.3" + inquirer "1.0.3" + needle "^2.0.1" + open "^0.0.5" + os-name "^1.0.3" + proxy-from-env "^1.0.0" + recursive-readdir "^2.2.1" + semver "^5.1.0" + snyk-config "1.0.1" + snyk-go-plugin "1.4.5" + snyk-gradle-plugin "1.2.0" + snyk-module "1.8.1" + snyk-mvn-plugin "1.1.1" + snyk-nuget-plugin "1.3.9" + snyk-php-plugin "1.3.2" + snyk-policy "^1.10.2" + snyk-python-plugin "1.5.7" + snyk-resolve "1.0.0" + snyk-resolve-deps "1.7.0" + snyk-sbt-plugin "1.2.5" + snyk-tree "^1.0.0" + snyk-try-require "^1.2.0" + tempfile "^1.1.1" + then-fs "^2.0.0" + undefsafe "0.0.3" + update-notifier "^0.5.0" + url "^0.11.0" + uuid "^3.0.1" + +source-map-support@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.3.tgz#2b3d5fff298cfa4d1afd7d4352d569e9a0158e76" + dependencies: + source-map "^0.6.0" + +source-map-support@^0.4.15: + version "0.4.18" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" + dependencies: + source-map "^0.5.6" + +source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + +spdx-correct@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82" + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz#2c7ae61056c714a5b9b9b2b2af7d311ef5c78fe9" + +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz#7a7cd28470cc6d3a1cfe6d66886f6bc430d3ac87" + +split2@^2.0.1, split2@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-2.2.0.tgz#186b2575bcf83e85b7d18465756238ee4ee42493" + dependencies: + through2 "^2.0.2" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +sshpk@^1.7.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.1.tgz#130f5975eddad963f1d56f92b9ac6c51fa9f83eb" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +stream-parser@~0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/stream-parser/-/stream-parser-0.3.1.tgz#1618548694420021a1182ff0af1911c129761773" + dependencies: + debug "2" + +stream-shift@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" + +string-length@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-1.0.1.tgz#56970fb1c38558e9e70b728bf3de269ac45adfac" + dependencies: + strip-ansi "^3.0.0" + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" + dependencies: + safe-buffer "~5.1.0" + +stringify-entities@^1.0.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-1.3.1.tgz#b150ec2d72ac4c1b5f324b51fb6b28c9cdff058c" + dependencies: + character-entities-html4 "^1.0.0" + character-entities-legacy "^1.0.0" + is-alphanumerical "^1.0.0" + is-hexadecimal "^1.0.0" + +stringstream@~0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991" + +strip-bom-buf@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-bom-buf/-/strip-bom-buf-1.0.0.tgz#1cb45aaf57530f4caf86c7f75179d2c9a51dd572" + dependencies: + is-utf8 "^0.2.1" + +strip-bom-stream@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom-stream/-/strip-bom-stream-3.0.0.tgz#956bcc5d84430f69256a90ed823765cd858e159c" + dependencies: + first-chunk-stream "^2.0.0" + strip-bom-buf "^1.0.0" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + dependencies: + is-utf8 "^0.2.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + dependencies: + get-stdin "^4.0.1" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +structured-source@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/structured-source/-/structured-source-3.0.2.tgz#dd802425e0f53dc4a6e7aca3752901a1ccda7af5" + dependencies: + boundary "^1.0.1" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +supports-color@^5.2.0, supports-color@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.3.0.tgz#5b24ac15db80fa927cf5227a4a33fd3c4c7676c0" + dependencies: + has-flag "^3.0.0" + +table@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36" + dependencies: + ajv "^5.2.3" + ajv-keywords "^2.1.0" + chalk "^2.1.0" + lodash "^4.17.4" + slice-ansi "1.0.0" + string-width "^2.1.1" + +table@^3.7.8: + version "3.8.3" + resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" + dependencies: + ajv "^4.7.0" + ajv-keywords "^1.0.0" + chalk "^1.1.1" + lodash "^4.0.0" + slice-ansi "0.0.4" + string-width "^2.0.0" + +tempfile@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/tempfile/-/tempfile-1.1.1.tgz#5bcc4eaecc4ab2c707d8bc11d99ccc9a2cb287f2" + dependencies: + os-tmpdir "^1.0.0" + uuid "^2.0.1" + +text-table@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + +then-fs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/then-fs/-/then-fs-2.0.0.tgz#72f792dd9d31705a91ae19ebfcf8b3f968c81da2" + dependencies: + promise ">=3.2 <8" + +through2@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" + dependencies: + readable-stream "^2.1.5" + xtend "~4.0.1" + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +timed-out@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-2.0.0.tgz#f38b0ae81d3747d628001f41dafc652ace671c0a" + +timed-out@^3.0.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-3.1.3.tgz#95860bfcc5c76c277f8f8326fd0f5b2e20eba217" + +timed-out@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + dependencies: + os-tmpdir "~1.0.2" + +to-fast-properties@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + +to-utf8@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/to-utf8/-/to-utf8-0.0.1.tgz#d17aea72ff2fba39b9e43601be7b3ff72e089852" + +toml@^2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/toml/-/toml-2.3.3.tgz#8d683d729577cb286231dfc7a8affe58d31728fb" + +tosource@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/tosource/-/tosource-1.0.0.tgz#42d88dd116618bcf00d6106dd5446f3427902ff1" + +tough-cookie@~2.3.3: + version "2.3.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" + dependencies: + punycode "^1.4.1" + +tr46@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + dependencies: + punycode "^2.1.0" + +traverse@^0.6.6: + version "0.6.6" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" + +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + +trim-right@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" + +trim-trailing-lines@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.0.tgz#7aefbb7808df9d669f6da2e438cac8c46ada7684" + +trim@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" + +trough@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.1.tgz#a9fd8b0394b0ae8fff82e0633a0a36ccad5b5f86" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + dependencies: + prelude-ls "~1.1.2" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +ua-parser-js@^0.7.9: + version "0.7.17" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac" + +undefsafe@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-0.0.3.tgz#ecca3a03e56b9af17385baac812ac83b994a962f" + +underscore@~1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8" + +underscore@~1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" + +unherit@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.0.tgz#6b9aaedfbf73df1756ad9e316dd981885840cd7d" + dependencies: + inherits "^2.0.1" + xtend "^4.0.1" + +unified@^4.1.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/unified/-/unified-4.2.1.tgz#76ff43aa8da430f6e7e4a55c84ebac2ad2cfcd2e" + dependencies: + bail "^1.0.0" + extend "^3.0.0" + has "^1.0.1" + once "^1.3.3" + trough "^1.0.0" + vfile "^1.0.0" + +unist-util-is@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-2.1.1.tgz#0c312629e3f960c66e931e812d3d80e77010947b" + +unist-util-remove-position@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-1.1.1.tgz#5a85c1555fc1ba0c101b86707d15e50fa4c871bb" + dependencies: + unist-util-visit "^1.1.0" + +unist-util-visit@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.3.0.tgz#41ca7c82981fd1ce6c762aac397fc24e35711444" + dependencies: + unist-util-is "^2.1.1" + +unzip-response@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-1.0.2.tgz#b984f0877fc0a89c2c773cc1ef7b5b232b5b06fe" + +unzip-response@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" + +upath@1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.4.tgz#ee2321ba0a786c50973db043a50b7bcba822361d" + +update-notifier@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-0.5.0.tgz#07b5dc2066b3627ab3b4f530130f7eddda07a4cc" + dependencies: + chalk "^1.0.0" + configstore "^1.0.0" + is-npm "^1.0.0" + latest-version "^1.0.0" + repeating "^1.1.2" + semver-diff "^2.0.0" + string-length "^1.0.0" + +update-notifier@^0.6.0: + version "0.6.3" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-0.6.3.tgz#776dec8daa13e962a341e8a1d98354306b67ae08" + dependencies: + boxen "^0.3.1" + chalk "^1.0.0" + configstore "^2.0.0" + is-npm "^1.0.0" + latest-version "^2.0.0" + semver-diff "^2.0.0" + +update-section@^0.3.0: + version "0.3.3" + resolved "https://registry.yarnpkg.com/update-section/-/update-section-0.3.3.tgz#458f17820d37820dc60e20b86d94391b00123158" + +url-parse-lax@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" + dependencies: + prepend-http "^1.0.1" + +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +user-home@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" + dependencies: + os-homedir "^1.0.0" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +uuid@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" + +uuid@^3.0.1, uuid@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" + +validate-npm-package-license@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338" + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +vfile-location@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-2.0.2.tgz#d3675c59c877498e492b4756ff65e4af1a752255" + +vfile@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-1.4.0.tgz#c0fd6fa484f8debdb771f68c31ed75d88da97fe7" + +wcwidth@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + dependencies: + defaults "^1.0.3" + +webidl-conversions@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + +whatwg-fetch@>=0.10.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" + +whatwg-url@6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.3.0.tgz#597ee5488371abe7922c843397ddec1ae94c048d" + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.0" + webidl-conversions "^4.0.1" + +which-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + +which@^1.2.9: + version "1.3.0" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" + dependencies: + isexe "^2.0.0" + +widest-line@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-1.0.0.tgz#0c09c85c2a94683d0d7eaf8ee097d564bf0e105c" + dependencies: + string-width "^1.0.1" + +win-release@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/win-release/-/win-release-1.1.1.tgz#5fa55e02be7ca934edfc12665632e849b72e5209" + dependencies: + semver "^5.0.1" + +window-size@^0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876" + +window-size@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075" + +wordwrap@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" + +wordwrap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +write-file-atomic@^1.1.2: + version "1.3.4" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.4.tgz#f807a4f0b1d9e913ae7a48112e6cc3af1991b45f" + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + slide "^1.1.5" + +write@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" + dependencies: + mkdirp "^0.5.1" + +xdg-basedir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-2.0.0.tgz#edbc903cc385fc04523d966a335504b5504d1bd2" + dependencies: + os-homedir "^1.0.0" + +xml2js@^0.4.17: + version "0.4.19" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + dependencies: + sax ">=0.6.0" + xmlbuilder "~9.0.1" + +xmlbuilder@~9.0.1: + version "9.0.7" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" + +xmldom@0.1.27: + version "0.1.27" + resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" + +xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + +y18n@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + +yargs-parser@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-2.4.1.tgz#85568de3cf150ff49fa51825f03a8c880ddcc5c4" + dependencies: + camelcase "^3.0.0" + lodash.assign "^4.0.6" + +yargs-parser@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077" + dependencies: + camelcase "^4.1.0" + +yargs@11.0.0, yargs@~11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.0.0.tgz#c052931006c5eee74610e5fc0354bedfd08a201b" + dependencies: + cliui "^4.0.0" + decamelize "^1.1.1" + find-up "^2.1.0" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^9.0.2" + +yargs@^4.3.2: + version "4.8.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-4.8.1.tgz#c0c42924ca4aaa6b0e6da1739dfb216439f9ddc0" + dependencies: + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + lodash.assign "^4.0.3" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.1" + which-module "^1.0.0" + window-size "^0.2.0" + y18n "^3.2.1" + yargs-parser "^2.4.1" + +yargs@~3.15.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.15.0.tgz#3d9446ef21fb3791b3985690662e4b9683c7f181" + dependencies: + camelcase "^1.0.2" + cliui "^2.1.0" + decamelize "^1.0.0" + window-size "^0.1.1" + +yauzl@2.9.1: + version "2.9.1" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.9.1.tgz#a81981ea70a57946133883f029c5821a89359a7f" + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.0.1" + +yazl@^2.3.1: + version "2.4.3" + resolved "https://registry.yarnpkg.com/yazl/-/yazl-2.4.3.tgz#ec26e5cc87d5601b9df8432dbdd3cd2e5173a071" + dependencies: + buffer-crc32 "~0.2.3" + +zip@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/zip/-/zip-1.2.0.tgz#ad0ad42265309be42eb56fc86194e17c24e66a9c" + dependencies: + bops "~0.1.1" diff --git a/moz/youtube$.js b/moz/youtube$.js new file mode 100644 index 0000000..7512c79 --- /dev/null +++ b/moz/youtube$.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'youtube' }))(); diff --git a/moz/youtube.js b/moz/youtube.js new file mode 100644 index 0000000..bf7a26b --- /dev/null +++ b/moz/youtube.js @@ -0,0 +1,139 @@ +let openedByUser = false, + listenersSet = false, + listenerInt; + +let script = { + "url": "*://www.youtube.com/.+", + + "timeout": 1000, + + "init": (ready, rerun = false) => { + let _title, _year, _image, R = RegExp; + + let options, type, + alternative = $('#offer-module-container[class*="movie-offer"], #offer-module-container[class*="unlimited-offer"]'); + + if($('.more-button:not(span), .less-button').empty || !$('.opened').empty || !$('iron-dropdown[class*="ytd"][aria-hidden]').empty) + return script.timeout; + + // open and close the meta-information + // open + $('.more-button:not(span)').first.click(); + // close + setTimeout(() => $('.less-button').first.click(), script.timeout); + + // try to not bug the page content too much, use an alternative method first (if applicable) + if(!alternative.empty && !rerun) { + alternative = alternative.first; + + let title = $('#title', alternative).first, + year = $('#info p', alternative).child(2).lastElementChild, + image = $('#img img', alternative).first, + type = /\bmovie-offer\b/i.test(alternative.classList)? 'movie': 'show'; + + if(!title || !year) + return -1; + + title = title.textContent; + year = year.textContent|0; + image = image.src; + + title = title.replace(R(`\\s*(\\(\\s*)?${ year }\\s*(\\))?`), ''); + + return { type, title, year, image }; + } + + type = script.getType(); + + if(type == 'error') + return -1; + + if(type == 'movie' || type == 'show') { + let title = $((type == 'movie'? '.title': '#owner-container, #header #main-title')).first, + year = $('#content ytd-expander').first, + image = $('#img img').first || { src: '' }; + + if(!title) + return -1; + + title = title.textContent.trim(); + year = (year)? + +year.textContent.replace(/[^]*(?:release|air) date\s+(?:(?:\d+\/\d+\/)?(\d{2,4}))[^]*/i, ($0, $1, $$, $_) => +$1 < 1000? 2000 + +$1: $1): + YEAR; + image = (image)? + image.src: + null; + + title = title.replace(R(`\\s*(\\(\\s*)?${ year }\\s*(\\))?`), ''); + + options = { type, title, year }; + } else if(type == 'list') { + let title = $('#title').first, + year = $('#stats *').child(2), + image = $('#thumbnail #img').first; + + if(!title) + return -1; + + title = title.textContent.trim(); + year = parseInt(year.textContent); + image = (image || {}).src; + type = 'show'; + + options = { type, title, year, image }; + } else { + return -1; + } + + if(!listenersSet) { + listenerInt = setInterval(() => { + let closed = 'collapsed' in $('ytd-expander').first.attributes; + + if(closed && !openedByUser) + script.init(true); + }, 10); + + $('ytd-expander').first.addEventListener('mouseup', event => { + let closed = 'collapsed' in $('ytd-expander').first.attributes; + + if(!closed) + openedByUser = true; + else + openedByUser = false; + }); + + listenersSet = true; + } else { + clearInterval(listenerInt); + } + + return options; + }, + + "getType": () => { + let title = $('.super-title, #title, #header #main-title').filter(e => e.textContent)[0], + subtitle = $('#header #main-title + #sub-title').filter(e => e.textContent)[0], + owner = $('#owner-container, #upload-info [href^="/channel/"]'); + + if(owner.empty) + return 'error'; + else + owner = owner.first.textContent.replace(/^\s+|\s+$/g, ''); + + let R = { + movie: /\byoutube movies\b/i, + show : /\b(s\d+\b.+\be\d+|season \d+)\b/i, + list : /\/playlist\b/, + }; + + return (R.movie.test(owner))? + 'movie': + ((title && R.show.test(title.textContent)) || (subtitle && R.show.test(subtitle.textContent)))? + 'show': + (title && R.list.test(top.location.pathname))? + 'list': + 'error'; + }, +}; + +// $('a[href*="/watch?v="]').forEach(element => element.onclick = event => open(event.target.href, '_self')); diff --git a/moz/youtube.png b/moz/youtube.png new file mode 100644 index 0000000..2758467 Binary files /dev/null and b/moz/youtube.png differ diff --git a/src.crx b/src.crx index 455fd7c..fad7fc3 100644 Binary files a/src.crx and b/src.crx differ diff --git a/src.zip b/src.zip index 65dd4da..12a6d71 100644 Binary files a/src.zip and b/src.zip differ diff --git a/src/background.js b/src/background.js index 4a90687..a872e3d 100644 --- a/src/background.js +++ b/src/background.js @@ -1,688 +1,1029 @@ /* global chrome */ -let NO_DEBUGGER = false; +let BACKGROUND_DEVELOPER = false; let external = {}, - parentItem, - saveItem, - terminal = - NO_DEBUGGER? - { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m }: - console; + __context_parent__, + __context_save_element__, + BACKGROUND_TERMINAL = + BACKGROUND_DEVELOPER? + console: + { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m }; let date = (new Date), - YEAR = date.getFullYear(), - MONTH = date.getMonth() + 1, - DATE = date.getDate(); + YEAR = date.getFullYear(), + MONTH = date.getMonth() + 1, + DATE = date.getDate(); + +let BACKGROUND_STORAGE = chrome.storage.sync || chrome.storage.local; +let BACKGROUND_CONFIGURATION; // returns the proper CORS mode of the URL let cors = url => ((/^(https|sftp)\b/i.test(url) || /\:(443|22)\b/? '': 'no-') + 'cors'); // Create a Crypto-Key -// new Key(number:integer, string) -> string +// new Key(number:integer, string:symbol) -> string class Key { - constructor(length = 8, symbol = '') { - let values = []; + constructor(length = 8, symbol = '') { + let values = []; - window.crypto.getRandomValues(new Uint32Array(16)).forEach((value, index, array) => values.push(value.toString(36))); + window.crypto.getRandomValues(new Uint32Array(16)).forEach((value, index, array) => values.push(value.toString(36))); - return this.length = length, this.value = values.join(symbol); - } + return this.length = length, this.value = values.join(symbol); + } - rehash(length, symbol) { - if(length) - /* Do nothing */; - else - length = this.length; + rehash(length, symbol) { + if(length <= 0) + length = this.length; - return this.value = new Key(length, symbol); - } + return this.value = new Key(length, symbol); + } } // Session instances let SessionKey = new Key(16), // create a session key - SessionState = false; // has this been run already? + SessionState = false; // has this been run already? // Generate request headers (for fetches) // new Headers({username, password}) -> object class Headers { - constructor(Authorization) { - let headers = { Accept: 'application/json' }; + constructor(Authorization) { + let headers = { Accept: 'application/json' }; - if (!Authorization) - return headers; + if (!Authorization) + return headers; - return { - Authorization: `Basic ${ btoa(`${ Authorization.username }:${ Authorization.password }`) }`, - ...headers - }; - } + return { + Authorization: `Basic ${ btoa(`${ Authorization.username }:${ Authorization.password }`) }`, + ...headers + }; + } } // Change the badge status // ChangeStatus({ MovieOrShowID, MovieOrShowTitle, MovieOrShowType, MovieOrShowIDProvider, MovieOrShowYear, LinkURL, FileType, FilePath }) -> undefined function ChangeStatus({ ITEM_ID, ITEM_TITLE, ITEM_TYPE, ID_PROVIDER, ITEM_YEAR, ITEM_URL = '', FILE_TYPE = '', FILE_PATH }) { - let FILE_TITLE = ITEM_TITLE.replace(/\-/g, ' ').replace(/[\s\:]{2,}/g, ' - ').replace(/[^\w\s\-\']+/g, ''), - // File friendly title - SEARCH_TITLE = ITEM_TITLE.replace(/[\-\s]+/g, '-').replace(/[^\w\-]+/g, ''), - // Search friendly title - SEARCH_PROVIDER = /[it]m/i.test(ID_PROVIDER)? 'GX': 'GG'; - - ITEM_ID = (ITEM_ID && !/^tt$/i.test(ITEM_ID)? ITEM_ID: '') + ''; - ITEM_ID = ITEM_ID.replace(/^.*\b(tt\d+)\b.*$/, '$1').replace(/^.*\bid=(\d+)\b.*$/, '$1').replace(/^.*(?:movie|tv|(?:tv-?)?(?:shows?|series|episodes?))\/(\d+).*$/, '$1'); - - external = { ...external, ID_PROVIDER, ITEM_ID, ITEM_TITLE, ITEM_YEAR, ITEM_URL, ITEM_TYPE, SEARCH_PROVIDER, SEARCH_TITLE, FILE_PATH, FILE_TITLE, FILE_TYPE }; - - chrome.browserAction.setBadgeText({ - text: ID_PROVIDER - }); - - chrome.browserAction.setBadgeBackgroundColor({ - color: (ITEM_ID? '#f45a26': '#666666') - }); - - chrome.contextMenus.update('W2P', { - title: `Find "${ ITEM_TITLE } (${ ITEM_YEAR || YEAR })"` - }); - - for(let array = 'IM TM TV'.split(' '), length = array.length, index = 0, item; index < length; index++) - chrome.contextMenus.update('W2P-' + (item = array[index]), { - title: ( - ((ID_PROVIDER == (item += 'Db')) && ITEM_ID)? - `Open in ${ item } (${ (+ITEM_ID? '#': '') + ITEM_ID })`: - `Find in ${ item }` - ), - checked: false - }); - - chrome.contextMenus.update('W2P-XX', { - title: `Find on ${ (SEARCH_PROVIDER == 'GX'? 'GoStream': 'Google') }`, - checked: false - }); + let FILE_TITLE = ITEM_TITLE.replace(/\-/g, ' ').replace(/[\s\:]{2,}/g, ' - ').replace(/[^\w\s\-\']+/g, ''), + // File friendly title + SEARCH_TITLE = ITEM_TITLE.replace(/[\-\s]+/g, '-').replace(/\s*&\s*/g, ' and ').replace(/[^\w\-\'\*\#]+/g, ''), + // Search friendly title + SEARCH_PROVIDER = /\b(tv|show|series)\b/i.test(ITEM_TYPE)? 'GG': /^im/i.test(ID_PROVIDER)? 'VO': /^tm/i.test(ID_PROVIDER)? 'GX': 'GG'; + + ITEM_ID = (ITEM_ID && !/^tt$/i.test(ITEM_ID)? ITEM_ID: '') + ''; + ITEM_ID = ITEM_ID.replace(/^.*\b(tt\d+)\b.*$/, '$1').replace(/^.*\bid=(\d+)\b.*$/, '$1').replace(/^.*(?:movie|tv|(?:tv-?)?(?:shows?|series|episodes?))\/(\d+).*$/, '$1'); + + external = { ...external, ID_PROVIDER, ITEM_ID, ITEM_TITLE, ITEM_YEAR, ITEM_URL, ITEM_TYPE, SEARCH_PROVIDER, SEARCH_TITLE, FILE_PATH, FILE_TITLE, FILE_TYPE }; + + chrome.browserAction.setBadgeText({ + text: ID_PROVIDER + }); + + chrome.browserAction.setBadgeBackgroundColor({ + color: (ITEM_ID? '#f45a26': '#666666') + }); + + chrome.contextMenus.update('W2P', { + title: `Find "${ ITEM_TITLE } (${ ITEM_YEAR || YEAR })"` + }); + + for(let databases = 'IM TM TV'.split(' '), length = databases.length, index = 0, database; index < length; index++) + chrome.contextMenus.update('W2P-' + (database = databases[index]), { + title: ( + ((ID_PROVIDER == (database += 'Db')) && ITEM_ID)? + `Open in ${ database } (${ (+ITEM_ID? '#': '') + ITEM_ID })`: + `Find in ${ database }` + ), + checked: false + }); + + chrome.contextMenus.update('W2P-XX', { + title: `Find on ${ (SEARCH_PROVIDER == 'VO'? 'Vumoo': SEARCH_PROVIDER == 'GX'? 'GoStream': 'Google') }`, + checked: false + }); +} + + +// get the saved options +function getConfiguration() { + return new Promise((resolve, reject) => { + function handleConfiguration(options) { + if((!options.plexToken || !options.servers) && !options.IGNORE_PLEX) + return reject(new Error('Required options are missing')), + null; + + let server, o; + + if(!options.IGNORE_PLEX) { + // For now we support only one Plex server, but the options already + // allow multiple for easy migration in the future. + server = options.servers[0]; + o = { + server: { + ...server, + // Compatibility for users who have not updated their settings yet. + connections: server.connections || [{ uri: server.url }] + }, + ...options + }; + } else { + o = options; + } + + resolve(o); + } + + BACKGROUND_STORAGE.get(null, options => { + if(chrome.runtime.lastError) + chrome.storage.local.get(null, handleOptions); + else + handleConfiguration(options); + }); + }); +} + +// self explanatory, returns an object; sets the configuration variable +function parseConfiguration() { + return getConfiguration().then(options => { + BACKGROUND_CONFIGURATION = options; + + if((BACKGROUND_DEVELOPER = options.DeveloperMode) && !parseConfiguration.gotConfig) { + parseConfiguration.gotConfig = true; + BACKGROUND_TERMINAL = + BACKGROUND_DEVELOPER? + console: + { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m }; + + BACKGROUND_TERMINAL.warn(`BACKGROUND_DEVELOPER: ${BACKGROUND_DEVELOPER}`); + } + + return options; + }, error => { throw error }); } +(async() => { + await parseConfiguration(); +})(); + +/** CouchPotato - Movies **/ // At this point you might want to think, WHY would you want to do // these requests in a background page instead of the content script? // This is because Movieo is served over HTTPS, so it won't accept requests to // HTTP servers. Unfortunately, many people use CouchPotato over HTTP. -function viewCouchPotato(request, sendResponse) { +function Open_CouchPotato(request, sendResponse) { fetch(`${ request.url }?id=${ request.imdbId }`, { headers: new Headers(request.basicAuth), - mode: cors(request.url) + mode: cors(request.url) }) - .then(response => response.json()) - .then(json => { - sendResponse({ success, status: (success? json.media.status: null) }); - }) - .catch(error => { - sendResponse({ error: String(error), location: 'viewCouchPotato' }); - }); + .then(response => response.json()) + .then(json => { + sendResponse({ success, status: (success? json.media.status: null) }); + }) + .catch(error => { + sendResponse({ error: String(error), location: '@0B: Open_CouchPotato' }); + }); } -function addCouchpotato(request, sendResponse) { +function Push_CouchPotato(request, sendResponse) { fetch(`${ request.url }?identifier=${ request.imdbId }`, { headers: new Headers(request.basicAuth), - mode: cors(request.url) + mode: cors(request.url) + }) + .then(response => response.json()) + .catch(error => sendResponse({ error: 'Item not found', location: '@0B: Push_CouchPotato => fetch.then.catch', silent: true })) + .then(response => { + sendResponse({ success: response.success }); }) - .then(response => response.json()) - .catch(error => sendResponse({ error: 'Item not found', location: 'addCouchpotato => fetch.then.catch', silent: true })) - .then(response => { - sendResponse({ success: response.success }); - }) - .catch(error => { - sendResponse({ error: String(error) , location: 'addCouchPotato'}); - }); + .catch(error => { + sendResponse({ error: String(error) , location: '@0B: Push_CouchPotato'}); + }); +} + +/** Watcher - Movies **/ +function Push_Watcher(request, sendResponse) { + let headers = { + 'Content-Type': 'application/json', + 'X-Api-Key': request.token, + ...(new Headers(request.basicAuth)) + }, + id = (/^(tt)?$/.test(request.imdbId)? request.tmdbId: request.imdbId), + // if the IMDbID is empty, jump to the TMDbID + query = (/^tt\d+$/i.test(id)? 'imdbid': /^\d+$/.test(id)? 'tmdbid': (id = encodeURI(`${request.title} ${request.year}`), 'term')), + // if the IMDbID is empty, use "&tmdbid={ id }" + // if the IMDbID isn't empty, use "&imdbid={ id }" + // otherwise, use "&term={ title } { year }" + debug = { headers, query, request }; + // setup a stack trace for debugging + + fetch(debug.url = `${ request.url }?apikey=${ request.token }&mode=addmovie&${ query }=${ id }`) + .then(response => response.json()) + .catch(error => sendResponse({ error: 'Movie not found', location: '@0B: Push_Watcher => fetch.then.catch', silent: true })) + .then(response => { + if((response.response + "") == "true") + return sendResponse({ + success: `Added to Watcher (${ request.StoragePath })` + }); + + throw new Error(response.error); + }) + .catch(error => { + sendResponse({ + error: String(error), + location: `@0B: Push_Watcher => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, + debug + }); + }); +} + +/** Radarr - Movies **/ +function Push_Radarr(request, sendResponse) { + let headers = { + 'Content-Type': 'application/json', + 'X-Api-Key': request.token, + ...(new Headers(request.basicAuth)) + }, + id = (/^(tt)?$/.test(request.imdbId)? request.tmdbId: request.imdbId), + // if the IMDbID is empty, jump to the TMDbID + query = (/^tt\d+$/i.test(id)? 'imdb?imdbid': /^\d+$/.test(id)? 'tmdb?tmdbid': (id = encodeURI(`${request.title} ${request.year}`), 'term')), + // if the IMDbID is empty, use "/tmdb?tmdbid={ id }" + // if the IMDbID isn't empty, use "/imdb?imdbid={ id }" + // otherwise, use "&term={ title } { year }" + debug = { headers, query, request }; + // setup a stack trace for debugging + + fetch(debug.url = `${ request.url }lookup/${ query }=${ id }&apikey=${ request.token }`) + .then(response => response.json()) + .catch(error => sendResponse({ error: 'Movie not found', location: '@0B: Push_Radarr => fetch.then.catch', silent: true })) + .then(data => { + let body, + // Monitor, search, and download movie ASAP + props = { + monitored: true, + minimumAvailability: 'preDB', + qualityProfileId: request.QualityID, + rootFolderPath: request.StoragePath, + addOptions: { + searchForMovie: true + } + }; + + if (!data instanceof Array && !data.length && !data.title) { + throw new Error('Movie not found'); + } else if(data.length) { + body = { + ...data[0], + ...props + }; + } else if(data.title) { + body = { + ...data, + ...props + }; + } + + BACKGROUND_TERMINAL.group('Generated URL'); + BACKGROUND_TERMINAL.log('URL', request.url); + BACKGROUND_TERMINAL.log('Head', headers); + BACKGROUND_TERMINAL.log('Body', body); + BACKGROUND_TERMINAL.groupEnd(); + + return debug.body = body; + }) + .then(body => { + return fetch(`${ request.url }?apikey=${ request.token }`, debug.requestHeaders = { + method: 'POST', + mode: cors(request.url), + body: JSON.stringify(body), + headers + }); + }) + .then(response => response.text()) + .then(data => { + debug.data = + data = JSON.parse(data || `{"path":"${ request.StoragePath.replace(/\\/g, '\\\\') }${ request.title } (${ request.year })"}`); + + if (data && data[0] && data[0].errorMessage) { + sendResponse({ + error: data[0].errorMessage, + location: `@0B: Push_Radarr => fetch("${ request.url }", { headers }).then(data => { if })`, + debug + }); + } else if (data && data.path) { + sendResponse({ + success: 'Added to ' + data.path + }); + } else { + sendResponse({ + error: 'Unknown error', + location: `@0B: Push_Radarr => fetch("${ request.url }", { headers }).then(data => { else })`, + debug + }); + } + }) + .catch(error => { + sendResponse({ + error: String(error), + location: `@0B: Push_Radarr => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, + debug + }); + }); +} + +/** Sonarr - TV Shows **/ +function Push_Sonarr(request, sendResponse) { + let headers = { + 'Content-Type': 'application/json', + 'X-Api-Key': request.token, + ...(new Headers(request.basicAuth)) + }, + id = request.tvdbId, + query = encodeURIComponent(`tvdb:${ id }`), + debug = { headers, query, request }; + // setup stack trace for debugging + + fetch(debug.url = `${ request.url }lookup?apikey=${ request.token }&term=${ query }`) + .then(response => response.json()) + .catch(error => sendResponse({ error: 'TV Show not found', location: '@0B: Push_Sonarr => fetch.then.catch', silent: true })) + .then(data => { + if (!data instanceof Array || !data.length) + throw new Error('TV Show not found'); + + // Monitor, search, and download series ASAP + let body = { + ...data[0], + monitored: true, + seasonFolder: true, + minimumAvailability: 'preDB', + qualityProfileId: request.QualityID, + rootFolderPath: request.StoragePath, + addOptions: { + searchForMissingEpisodes: true + } + }; + + BACKGROUND_TERMINAL.group('Generated URL'); + BACKGROUND_TERMINAL.log('URL', request.url); + BACKGROUND_TERMINAL.log('Head', headers); + BACKGROUND_TERMINAL.log('Body', body); + BACKGROUND_TERMINAL.groupEnd(); + + return debug.body = body; + }) + .then(body => { + return fetch(`${ request.url }?apikey=${ request.token }`, debug.requestHeaders = { + method: 'POST', + mode: cors(request.url), + body: JSON.stringify(body), + headers + }); + }) + .then(response => response.text()) + .then(data => { + debug.data = + data = JSON.parse(data || `{"path":"${ request.StoragePath.replace(/\\/g, '\\\\') }${ request.title } (${ request.year })"}`); + + if (data && data[0] && data[0].errorMessage) { + sendResponse({ + error: data[0].errorMessage, + location: `@0B: Push_Sonarr => fetch("${ request.url }", { headers }).then(data => { if })`, + debug + }); + } else if (data && data.path) { + sendResponse({ + success: 'Added to ' + data.path + }); + } else { + sendResponse({ + error: 'Unknown error', + location: `@0B: Push_Sonarr => fetch("${ request.url }", { headers }).then(data => { else })`, + debug + }); + } + }) + .catch(error => { + sendResponse({ + error: String(error), + location: `@0B: Push_Sonarr => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, + debug + }); + }); } -function addWatcher(request, sendResponse) { - let headers = { - 'Content-Type': 'application/json', - 'X-Api-Key': request.token, - ...(new Headers(request.basicAuth)) - }, - id = (/^(tt)?$/.test(request.imdbId)? request.tmdbId: request.imdbId), - // if the IMDbID is empty, jump to the TMDbID - query = (/^tt\d+$/i.test(id)? 'imdbid': /^\d+$/.test(id)? 'tmdbid': (id = encodeURI(`${request.title} ${request.year}`), 'term')), - // if the IMDbID is empty, use "&tmdbid={ id }" - // if the IMDbID isn't empty, use "&imdbid={ id }" - // otherwise, use "&term={ title } { year }" - debug = { headers, query, request }; - // setup a stack trace for debugging - - fetch(debug.url = `${ request.url }?apikey=${ request.token }&mode=addmovie&${ query }=${ id }`) - .then(response => response.json()) - .catch(error => sendResponse({ error: 'Movie not found', location: 'addWatcher => fetch.then.catch', silent: true })) - .then(response => { - if((response.response + "") == "true") - return sendResponse({ - success: `Added to Watcher (${ request.StoragePath })` - }); - - throw new Error(response.error); - }) - .catch(error => { - sendResponse({ - error: String(error), - location: `addWatcher => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, - debug - }); - }); +/** Medusa - TV Shows **/ +function Push_Medusa(request, sendResponse) { + let headers = { + 'Content-Type': 'application/json', + 'X-Api-Key': request.token, + ...(new Headers(request.basicAuth)) + }, + id = request.tvdbId, + query = request.title.replace(/\s+/g, '+'), + debug = { headers, query, request }; + // setup stack trace for debugging + + fetch(debug.url = `${ request.root }internal/searchIndexersForShowName?api_key=${ request.token }&indexerId=0&query=${ query }`) + .then(response => response.json()) + .catch(error => sendResponse({ error: 'TV Show not found', location: '@0B: Push_Medusa => fetch.then.catch', silent: true })) + .then(data => { + data = data.results; + + if (!data instanceof Array || !data.length) + throw new Error('TV Show not found'); + + // Monitor, search, and download series ASAP + let body = data[0].join('|'); + + BACKGROUND_TERMINAL.group('Generated URL'); + BACKGROUND_TERMINAL.log('URL', request.url); + BACKGROUND_TERMINAL.log('Head', headers); + BACKGROUND_TERMINAL.log('Body', body); + BACKGROUND_TERMINAL.groupEnd(); + + return debug.body = body; + }) + .then(body => { + return fetch(`${ request.url }`, debug.requestHeaders = { + method: 'POST', + mode: cors(request.url), + body: JSON.stringify({ id: { tvdb: request.tvdbId } }), + headers + }); + }) + .then(response => response.text()) + .then(data => { + let path = request.StoragePath.replace(/\\?$/, '\\'); + + debug.data = + data = JSON.parse(data || `{"path":"${ path }${ request.title } (${ request.year })"}`); + + if (data && data.error) { + sendResponse({ + error: data.error, + location: `@0B: Push_Medusa => fetch("${ request.url }", { headers }).then(data => { if })`, + debug + }); + } else if (data && data.id) { + sendResponse({ + success: `Added to ${ path }${ request.title }(${ request.year })` + }); + } else { + sendResponse({ + error: 'Unknown error', + location: `@0B: Push_Medusa => fetch("${ request.url }", { headers }).then(data => { else })`, + debug + }); + } + }) + .catch(error => { + sendResponse({ + error: String(error), + location: `@0B: Push_Medusa => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, + debug + }); + }); } -function addRadarr(request, sendResponse) { - let headers = { - 'Content-Type': 'application/json', - 'X-Api-Key': request.token, - ...(new Headers(request.basicAuth)) - }, - id = (/^(tt)?$/.test(request.imdbId)? request.tmdbId: request.imdbId), - // if the IMDbID is empty, jump to the TMDbID - query = (/^tt\d+$/i.test(id)? 'imdb?imdbid': /^\d+$/.test(id)? 'tmdb?tmdbid': (id = encodeURI(`${request.title} ${request.year}`), 'term')), - // if the IMDbID is empty, use "/tmdb?tmdbid={ id }" - // if the IMDbID isn't empty, use "/imdb?imdbid={ id }" - // otherwise, use "&term={ title } { year }" - debug = { headers, query, request }; - // setup a stack trace for debugging - - fetch(debug.url = `${ request.url }lookup/${ query }=${ id }&apikey=${ request.token }`) - .then(response => response.json()) - .catch(error => sendResponse({ error: 'Movie not found', location: 'addRadarr => fetch.then.catch', silent: true })) - .then(data => { - let body, - // Monitor, search, and download movie ASAP - props = { - monitored: true, - minimumAvailability: 'preDB', - qualityProfileId: request.QualityID, - rootFolderPath: request.StoragePath, - addOptions: { - searchForMovie: true - } - }; - - if (!data instanceof Array && !data.length && !data.title) { - throw new Error('Movie not found'); - } else if(data.length) { - body = { - ...data[0], - ...props - }; - } else if(data.title) { - body = { - ...data, - ...props - }; - } - - terminal.group('Generated URL'); - terminal.log('URL', request.url); - terminal.log('Head', headers); - terminal.log('Body', body); - terminal.groupEnd(); - - return debug.body = body; - }) - .then(body => { - return fetch(`${ request.url }?apikey=${ request.token }`, debug.requestHeaders = { - method: 'POST', - mode: cors(request.url), - body: JSON.stringify(body), - headers - }); - }) - .then(response => response.text()) - .then(data => { - debug.data = - data = JSON.parse(data || `{"path":"${ request.StoragePath.replace(/\\/g, '\\\\') }${ request.title } (${ request.year })"}`); - - if (data && data[0] && data[0].errorMessage) { - sendResponse({ - error: data[0].errorMessage, - location: `addRadarr => fetch("${ request.url }", { headers }).then(data => { if })`, - debug - }); - } else if (data && data.path) { - sendResponse({ - success: 'Added to ' + data.path - }); - } else { - sendResponse({ - error: 'Unknown error', - location: `addRadarr => fetch("${ request.url }", { headers }).then(data => { else })`, - debug - }); - } - }) - .catch(error => { - sendResponse({ - error: String(error), - location: `addRadarr => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, - debug - }); - }); +/** Medusa - TV Shows **/ +function addMedusa(request, sendResponse) { + let headers = { + 'Content-Type': 'application/json', + 'X-Api-Key': request.token, + ...(new Headers(request.basicAuth)) + }, + id = request.tvdbId, + query = request.title.replace(/\s+/g, '+'), + debug = { headers, query, request }; + // setup stack trace for debugging + + fetch(debug.url = `${ request.root }internal/searchIndexersForShowName?api_key=${ request.token }&indexerId=0&query=${ query }`) + .then(response => response.json()) + .catch(error => sendResponse({ error: 'TV Show not found', location: 'addMedusa => fetch.then.catch', silent: true })) + .then(data => { + data = data.results; + + if (!data instanceof Array || !data.length) + throw new Error('TV Show not found'); + + // Monitor, search, and download series ASAP + let body = data[0].join('|'); + + BACKGROUND_TERMINAL.group('Generated URL'); + BACKGROUND_TERMINAL.log('URL', request.url); + BACKGROUND_TERMINAL.log('Head', headers); + BACKGROUND_TERMINAL.log('Body', body); + BACKGROUND_TERMINAL.groupEnd(); + + return debug.body = body; + }) + .then(body => { + return fetch(`${ request.url }`, debug.requestHeaders = { + method: 'POST', + mode: cors(request.url), + body: JSON.stringify({ id: { tvdb: request.tvdbId } }), + headers + }); + }) + .then(response => response.text()) + .then(data => { + let path = request.StoragePath.replace(/\\?$/, '\\'); + + debug.data = + data = JSON.parse(data || `{"path":"${ path }${ request.title } (${ request.year })"}`); + + if (data && data.error) { + sendResponse({ + error: data.error, + location: `addMedusa => fetch("${ request.url }", { headers }).then(data => { if })`, + debug + }); + } else if (data && data.id) { + sendResponse({ + success: `Added to ${ path }${ request.title }(${ request.year })` + }); + } else { + sendResponse({ + error: 'Unknown error', + location: `addMedusa => fetch("${ request.url }", { headers }).then(data => { else })`, + debug + }); + } + }) + .catch(error => { + sendResponse({ + error: String(error), + location: `addMedusa => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, + debug + }); + }); } -function addSonarr(request, sendResponse) { - let headers = { - 'Content-Type': 'application/json', - 'X-Api-Key': request.token, - ...(new Headers(request.basicAuth)) - }, - id = request.tvdbId, - query = encodeURIComponent(`tvdb:${ id }`), - debug = { headers, query, request }; - // setup stack trace for debugging - - fetch(debug.url = `${ request.url }lookup?apikey=${ request.token }&term=${ query }`) - .then(response => response.json()) - .catch(error => sendResponse({ error: 'TV Show not found', location: 'addSonarr => fetch.then.catch', silent: true })) - .then(data => { - if (!data instanceof Array || !data.length) - throw new Error('TV Show not found'); - - // Monitor, search, and download series ASAP - let body = { - ...data[0], - monitored: true, - minimumAvailability: 'preDB', - qualityProfileId: request.QualityID, - rootFolderPath: request.StoragePath, - addOptions: { - searchForMissingEpisodes: true - } - }; - - terminal.group('Generated URL'); - terminal.log('URL', request.url); - terminal.log('Head', headers); - terminal.log('Body', body); - terminal.groupEnd(); - - return debug.body = body; - }) - .then(body => { - return fetch(`${ request.url }?apikey=${ request.token }`, debug.requestHeaders = { - method: 'POST', - mode: cors(request.url), - body: JSON.stringify(body), - headers - }); - }) - .then(response => response.text()) - .then(data => { - debug.data = - data = JSON.parse(data || `{"path":"${ request.StoragePath.replace(/\\/g, '\\\\') }${ request.title } (${ request.year })"}`); - - if (data && data[0] && data[0].errorMessage) { - sendResponse({ - error: data[0].errorMessage, - location: `addSonarr => fetch("${ request.url }", { headers }).then(data => { if })`, - debug - }); - } else if (data && data.path) { - sendResponse({ - success: 'Added to ' + data.path - }); - } else { - sendResponse({ - error: 'Unknown error', - location: `addSonarr => fetch("${ request.url }", { headers }).then(data => { else })`, - debug - }); - } - }) - .catch(error => { - sendResponse({ - error: String(error), - location: `addSonarr => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, - debug - }); - }); +/** Sick Beard - TV Shows **/ +function Push_SickBeard(request, sendResponse) { + let headers = { + 'Content-Type': 'application/json', + 'X-Api-Key': request.token, + ...(new Headers(request.basicAuth)) + }, + id = request.tvdbId, + query = `tvdbid=${ id }`, + path = (`${ request.StoragePath }\\${ request.title }`).replace(/\\\\/g, '\\'), + debug = { headers, query, request }; + // setup stack trace for debugging + + fetch(debug.url = `${ request.url }?cmd=sb.searchtvdb&${ query }`) + .then(response => response.json()) + .catch(error => sendResponse({ error: 'TV Show not found', location: '@0B: Push_SickBeard => fetch.then.catch', silent: true })) + .then(data => { + if (!/^success$/i.test(data.result)) + throw new Error('TV Show not found'); + + data = data.data.results; + + // Monitor, search, and download series ASAP + let body = formify({ + tvdbid: id, + initial: request.QualityID, + location: encodeURIComponent(path), + status: 'wanted', + }); + + BACKGROUND_TERMINAL.group('Generated URL'); + BACKGROUND_TERMINAL.log('URL', request.url); + BACKGROUND_TERMINAL.log('Head', headers); + BACKGROUND_TERMINAL.log('Body', body); + BACKGROUND_TERMINAL.groupEnd(); + + return debug.body = body; + }) + .then(async body => { + await fetch(`${ request.url }?cmd=sb.addrootdir&${ body }`); + + return fetch(`${ request.url }?cmd=show.${ request.exists? 'addexisting': 'addnew' }&${ body }`, debug.requestHeaders = { + method: 'POST', + mode: cors(request.url), + // body: JSON.stringify(body), + headers + }); + }) + .then(response => response.text()) + .then(results => { + debug.data = + results = JSON.parse(results || `{"data":{},message:"",result:""}`); + + let { data, message, result } = results; + + data.path = `${ request.StoragePath }${ request.title } (${ request.year })`; + + if (data && !/^success$/i.test(result) && message) { + sendResponse({ + error: message, + location: `@0B: Push_SickBeard => fetch("${ request.url }", { headers }).then(results => { if })`, + debug + }); + } else if (data && data.path) { + sendResponse({ + success: 'Added to ' + data.path + }); + } else { + sendResponse({ + error: 'Unknown error', + location: `@0B: Push_SickBeard => fetch("${ request.url }", { headers }).then(results => { else })`, + debug + }); + } + }) + .catch(error => { + sendResponse({ + error: String(error), + location: `@0B: Push_SickBeard => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, + debug + }); + }); } -function addOmbi(request, sendResponse) { - let headers = { - 'Content-Type': 'application/json', - 'ApiKey': request.token, - ...(new Headers) - }, - type = request.contentType, - id = (type == 'movie'? request.tmdbId: request.tvdbId), - body = ({ [type == 'movie'? 'theMovieDbId': 'theTvDbId']: id }), - debug = { headers, request }; - // setup stack trace for debugging - - if(request.contentType == 'movie' && (id || null) === null) - sendResponse({ error: 'Invalid TMDbID', location: 'addOmbi => if', silent: true }); - else if((id || null) === null) - sendResponse({ error: 'Invalid TVDbID', location: 'addOmbi => else if', silent: true }); - - fetch(debug.url = request.url, { - method: 'POST', - mode: cors(request.url), - body: JSON.stringify(body), - headers - }) - .catch(error => sendResponse({ error: `${ type } not found`, location: 'addOmbi => fetch.then.catch', silent: true })) - .then(response => response.text()) - .then(data => { - debug.data = - data = JSON.parse(data); - - if (data && data.isError) { - if(/already +been +requested/i.test(data.errorMessage)) - sendResponse({ - success: 'Already requested on Ombi' - }); - else - sendResponse({ - error: data.errorMessage, - location: `addOmbi => fetch("${ request.url }", { headers }).then(data => { if })`, - debug - }); - } else if (data && data.path) { - sendResponse({ - success: 'Added to Ombi' - }); - } else { - sendResponse({ - error: 'Unknown error', - location: `addOmbi => fetch("${ request.url }", { headers }).then(data => { else })`, - debug - }); - } - }) - .catch(error => { - sendResponse({ - error: String(error), - location: `addOmbi => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, - debug - }); - }); +/** Ombi* - TV Shows/Movies **/ +function Push_Ombi(request, sendResponse) { + let headers = { + 'Content-Type': 'application/json', + 'ApiKey': request.token, + ...(new Headers) + }, + type = request.contentType, + id = (type == 'movie'? request.tmdbId: request.tvdbId), + body = ({ [type == 'movie'? 'theMovieDbId': 'theTvDbId']: id }), + debug = { headers, request }; + // setup stack trace for debugging + + if(request.contentType == 'movie' && (id || null) === null) + sendResponse({ error: 'Invalid TMDbID', location: '@0B: Push_Ombi => if', silent: true }); + else if((id || null) === null) + sendResponse({ error: 'Invalid TVDbID', location: '@0B: Push_Ombi => else if', silent: true }); + + fetch(debug.url = request.url, { + method: 'POST', + mode: cors(request.url), + body: JSON.stringify(body), + headers + }) + .catch(error => sendResponse({ error: `${ type } not found`, location: '@0B: Push_Ombi => fetch.then.catch', silent: true })) + .then(response => response.text()) + .then(data => { + debug.data = + data = JSON.parse(data); + + if (data && data.isError) { + if(/already +been +requested/i.test(data.errorMessage)) + sendResponse({ + success: 'Already requested on Ombi' + }); + else + sendResponse({ + error: data.errorMessage, + location: `@0B: Push_Ombi => fetch("${ request.url }", { headers }).then(data => { if })`, + debug + }); + } else if (data && data.path) { + sendResponse({ + success: 'Added to Ombi' + }); + } else { + sendResponse({ + error: 'Unknown error', + location: `@0B: Push_Ombi => fetch("${ request.url }", { headers }).then(data => { else })`, + debug + }); + } + }) + .catch(error => { + sendResponse({ + error: String(error), + location: `@0B: Push_Ombi => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, + debug + }); + }); } // Unfortunately the native Promise.race does not work as you would suspect. // If one promise (Plex request) fails, we still want the other requests to continue racing. // See https://www.jcore.com/2016/12/18/promise-me-you-wont-use-promise-race/ for an explanation -function promiseRace(promises) { - if (!~promises.length) { - return Promise.reject('Cannot start a race without promises!'); - } - - // There is no way to know which promise is rejected. - // So we map it to a new promise to return the index when it fails - let Promises = promises.map((promise, index) => - promise.catch(() => { - throw index; - }) - ); - - return Promise.race(Promises) - .catch(index => { - // The promise has rejected, remove it from the list of promises and just continue the race. - let promise = promises.splice(index, 1)[0]; - - promise.catch(error => terminal.log(`Plex request #${ index } failed:`, error)); - return promiseRace(promises); - }); +function PromiseRace(promises) { + if (!~promises.length) { + return Promise.reject('Cannot start a race without promises!'); + } + + // There is no way to know which promise is rejected. + // So we map it to a new promise to return the index when it fails + let Promises = promises.map((promise, index) => + promise.catch(() => { + throw index; + }) + ); + + return Promise.race(Promises) + .catch(index => { + // The promise has rejected, remove it from the list of promises and just continue the race. + let promise = promises.splice(index, 1)[0]; + + promise.catch(error => BACKGROUND_TERMINAL.log(`Plex request #${ index } failed:`, error)); + return PromiseRace(promises); + }); } -function $searchPlex(connection, headers, options) { - let type = options.type || 'movie', - url = `${ connection.uri }/hubs/search`, - field = options.field || 'title'; - - if(!options.title) - return {}; - - if(/movie|film|cinema|theat[re]{2}/i.test(type)) - type = 'movie'; - else if(/tv|show|series|episode/i.test(type)) - type = 'show'; - - // Letterboxd can contain special white-space characters. Plex doesn't like this. - let title = encodeURIComponent(options.title.replace(/\s+/g, ' ')), - finalURL = `${ url }?query=${ field }:${ title }`; - - return fetch(finalURL, { headers }) - .then(response => response.json()) - .then(data => { - let Hub = data.MediaContainer.Hub.find(hub => hub.type === type); - - if (!Hub || !Hub.Metadata) { - return { found: false }; - } - - // We only want to search in Plex libraries with the type "Movie", i.e. not the type "Other Videos". - // Weirdly enough Plex doesn't seem to have an easy way to filter those libraries so we invent our own hack. - let movies = Hub.Metadata.filter( - meta => - meta.Directory || - meta.Genre || - meta.Country || - meta.Role || - meta.Writer - ), - strip = (string) => string.replace(/\W+/g, '').toLowerCase(); - - // This is messed up, but Plex's definition of a year is year when it was available, - // not when it was released (which is Movieo's definition). - // For examples, see Bone Tomahawk, The Big Short, The Hateful Eight. - // So we'll first try to find the movie with the given year, and then + 1 it. - // Added [strip] to prevent mix-ups, see: "Kingsman: The Golden Circle" v. "The Circle" - let media = movies.find(meta => ((meta.year == +options.year) && strip(meta.title) == strip(options.title))), - key = null; - - if (!media) { - media = movies.find(meta => ((meta.year == +options.year + 1) && strip(meta.title) == strip(options.title))); - } - - key = !!media? media.key.replace('/children', ''): key; - - return { - found: !!media, - key - }; - }); +function $Search_Plex(connection, headers, options) { + let type = options.type || 'movie', + url = `${ connection.uri }/hubs/search`, + field = options.field || 'title'; + + if(!options.title) + return {}; + + if(/movie|film|cinema|theat[re]{2}/i.test(type)) + type = 'movie'; + else if(/tv|show|series|episode/i.test(type)) + type = 'show'; + + // Letterboxd can contain special white-space characters. Plex doesn't like this. + let title = encodeURIComponent(options.title.replace(/\s+/g, ' ')), + finalURL = `${ url }?query=${ field }:${ title }`; + + // BACKGROUND_TERMINAL.warn(`Fetching <${ JSON.stringify(headers) } ${ finalURL } >`); + return fetch(finalURL, { headers }) + .then(response => response.json()) + .then(data => { + let Hub = data.MediaContainer.Hub.find(hub => hub.type === type); + + if (!Hub || !Hub.Metadata) { + return { found: false }; + } + + // We only want to search in Plex libraries with the type "Movie", i.e. not the type "Other Videos". + // Weirdly enough Plex doesn't seem to have an easy way to filter those libraries so we invent our own hack. + let movies = Hub.Metadata.filter( + meta => + meta.Directory || + meta.Genre || + meta.Country || + meta.Role || + meta.Writer + ), + strip = (string) => string.replace(/\W+/g, '').toLowerCase(); + + // This is messed up, but Plex's definition of a year is year when it was available, + // not when it was released (which is Movieo's definition). + // For examples, see Bone Tomahawk, The Big Short, The Hateful Eight. + // So we'll first try to find the movie with the given year, and then + 1 it. + // Added [strip] to prevent mix-ups, see: "Kingsman: The Golden Circle" v. "The Circle" + let media = movies.find(meta => ((meta.year == +options.year) && strip(meta.title) == strip(options.title))), + key = null; + + if (!media) { + media = movies.find(meta => ((meta.year == +options.year + 1) && strip(meta.title) == strip(options.title))); + } + + key = !!media? media.key.replace('/children', ''): key; + + return { + found: !!media, + key + }; + }) + .catch(error => { throw error }); } -async function searchPlex(request, sendResponse) { - let { options, serverConfig } = request, - headers = { - 'X-Plex-Token': serverConfig.token, - 'Accept': 'application/json' - }; - - // Try all Plex connection URLs - let requests = serverConfig.connections.map(connection => - $searchPlex(connection, headers, options) - ); - - try { - // See what connection URL finishes the request first and pick that one. - // TODO: optimally, as soon as the first request is finished, all other requests would be cancelled using AbortController. - let result = await promiseRace(requests); - - sendResponse(result); - } catch (error) { - sendResponse({ error: String(error), location: 'searchPlex' }); - } +async function Search_Plex(request, sendResponse) { + let { options, serverConfig } = request, + headers = { + 'X-Plex-Token': serverConfig.token, + 'Accept': 'application/json' + }; + + // Try all Plex connection URLs + let requests = serverConfig.connections.map(connection => + $Search_Plex(connection, headers, options) + ); + + try { + // See what connection URL finishes the request first and pick that one. + // TODO: optimally, as soon as the first request is finished, all other requests would be cancelled using AbortController. + let result = await PromiseRace(requests); + + sendResponse(result); + } catch (error) { + sendResponse({ error: String(error), location: '@0B: Search_Plex' }); + } } // Chrome is f**king retarted... // Instead of having an object returned (for the context-menu) // You have to make API calls on ALL clicks... chrome.contextMenus.onClicked.addListener(item => { - if(!/^W2P/i.test(item.menuItemId)) return; - - let url = "", dnl = false, - db = item.menuItemId.slice(-2).toLowerCase(), - pv = external.ID_PROVIDER.slice(0, 2).toLowerCase(), - qu = external.ITEM_ID, - tl = external.SEARCH_TITLE, - yr = external.ITEM_YEAR, - tt = external.ITEM_TITLE, - lt = external.FILE_TITLE, - ft = external.FILE_TYPE, - fp = external.FILE_PATH, - p = (s, r = '+') => s.replace(/-/g, r); - - switch(db) { - case 'im': - url = (qu && pv == 'im')? - `imdb.com/title/${ qu }/`: - `imdb.com/find?ref_=nv_sr_fn&s=all&q=${ tl }`; - break; - case 'tm': - url = (qu && pv == 'tm')? - `themoviedb.org/${ external.ITEM_TYPE == 'show'? 'tv': 'movie' }/${ qu }`: - `themoviedb.org/search?query=${ tl }`; - break; - case 'tv': - url = (qu && pv == 'tv')? - `thetvdb.com/series/${ tl }#${ qu }`: // TVDb accepts either: a title, or a series number... but only one - `thetvdb.com/search?q=${ p(tl) }`; - break; - case 'xx': - url = external.SEARCH_PROVIDER == 'GX'? - `gostream.site/?s=${ p(tl) }`: - `google.com/search?q="${ p(tl, ' ') } ${ yr }"+${ pv }db`; - break; - case 'dl': - dnl = true; - url = external.ITEM_URL; - break; - default: return; break; - } - - if(!dnl) - window.open(`https://${ url }`, '_blank'); - else if (dnl) - // try/catch won't work here, so use the first download's callback as an error catcher - chrome.downloads.download({ - url: item.href, - filename: `${ fp }${ lt } (${ yr }).${ ft }`, - saveAs: true - }, id => { - if(id == undefined || id == null) - chrome.downloads.download({ - url: item.href, - saveAs: true - }); - }); + if(!/^W2P/i.test(item.menuItemId)) return; + + let url = "", dnl = false, + db = item.menuItemId.slice(-2).toLowerCase(), + pv = external.ID_PROVIDER.slice(0, 2).toLowerCase(), + qu = external.ITEM_ID, + tl = external.SEARCH_TITLE, + yr = external.ITEM_YEAR, + tt = external.ITEM_TITLE, + lt = external.FILE_TITLE, + ft = external.FILE_TYPE, + fp = external.FILE_PATH, + p = (s, r = '+') => s.replace(/-/g, r); + + switch(db) { + case 'im': + url = (qu && pv == 'im')? + `imdb.com/title/${ qu }/`: + `imdb.com/find?ref_=nv_sr_fn&s=all&q=${ tt }`; + break; + case 'tm': + url = (qu && pv == 'tm')? + `themoviedb.org/${ external.ITEM_TYPE == 'show'? 'tv': 'movie' }/${ qu }`: + `themoviedb.org/search?query=${ tt }`; + break; + case 'tv': + url = (qu && pv == 'tv')? + `thetvdb.com/series/${ tl }#${ qu }`: // TVDb accepts either: a title, or a series number... but only one + `thetvdb.com/search?q=${ p(tl) }`; + break; + case 'xx': + url = external.SEARCH_PROVIDER == 'VO'? + `google.com/search?q=${ p(tl) }+site:vumoo.to`: + external.SEARCH_PROVIDER == 'GX'? + `gostream.site/?s=${ p(tl) }`: + `google.com/search?q="${ p(tl, ' ') } ${ yr }"+${ pv }db`; + break; + case 'dl': + dnl = true; + url = external.ITEM_URL; + break; + default: return; break; + } + + if(!dnl) + window.open(`https://${ url }`, '_blank'); + else if (dnl) + // try/catch won't work here, so use the first download's callback as an error catcher + chrome.downloads.download({ + url: item.href, + filename: `${ fp }${ lt } (${ yr }).${ ft }`, + saveAs: true + }, id => { + if(id == undefined || id == null) + chrome.downloads.download({ + url: item.href, + saveAs: true + }); + }); }); chrome.runtime.onMessage.addListener((request, sender, callback) => { - terminal.log('From: ' + sender); - - let item = (request? request.options || request: {}), - ITEM_TITLE = item.title, - ITEM_YEAR = item.year, - ITEM_TYPE = item.type, - ID_PROVIDER = (i=>{for(let p in i)if(/^TVDb/i.test(p)&&i[p])return'TVDb';else if(/^TMDb/i.test(p)&&i[p])return'TMDb';return'IMDb'})(item), - ITEM_URL = item.href || '', - FILE_TYPE = (item.tail || 'mp4'), - FILE_PATH = item.path || '', - ITEM_ID = ((i, I)=>{for(let p in i)if(RegExp('^'+I,'i').test(p))return i[p]})(item, ID_PROVIDER); - - try { - switch (request.type) { - case 'SEARCH_PLEX': - searchPlex(request, callback); - return true; - case 'VIEW_COUCHPOTATO': - viewCouchPotato(request, callback); - return true; - case 'ADD_COUCHPOTATO': - addCouchpotato(request, callback); - return true; - case 'ADD_RADARR': - addRadarr(request, callback); - return true; - case 'ADD_SONARR': - addSonarr(request, callback); - return true; - case 'ADD_WATCHER': - addWatcher(request, callback); - return true; - case 'ADD_OMBI': - addOmbi(request, callback); - return true; - case 'OPEN_OPTIONS': - chrome.runtime.openOptionsPage(); - return true; - case 'SEARCH_FOR': - if(ITEM_TITLE && ITEM_TYPE) - ChangeStatus({ ITEM_ID, ITEM_TITLE, ITEM_TYPE, ID_PROVIDER, ITEM_YEAR, ITEM_URL, FILE_TYPE, FILE_PATH }); - return true; - case 'SAVE_AS': - chrome.contextMenus.update('W2P-DL', { - title: `Save as "${ ITEM_TITLE } (${ ITEM_YEAR })" (${ FILE_TYPE })` - }); - return true; - case 'DOWNLOAD_FILE': - - let FILE_TITLE = ITEM_TITLE.replace(/\-/g, ' ').replace(/[\s\:]{2,}/g, ' - ').replace(/[^\w\s\-\']+/g, ''); - - // no try/catch, use callback for that - chrome.downloads.download({ - url: item.href, - filename: `${ FILE_TITLE } (${ ITEM_YEAR }).${ FILE_TYPE }`, - saveAs: true - }, id => { - // Error Occured - if(id == undefined || id == null) - chrome.downloads.download({ - url: item.href, - filename: `${ FILE_TITLE } (${ ITEM_YEAR })`, - saveAs: true - }); - }); - return true; - default: - terminal.warn(`Unknown event [${ request.type }]`); - return false; - } - } catch (error) { - return callback(String(error)); - } + BACKGROUND_TERMINAL.log('From: ' + sender); + + let item = (request? request.options || request: {}), + ITEM_TITLE = item.title, + ITEM_YEAR = item.year, + ITEM_TYPE = item.type, + ID_PROVIDER = (i=>{for(let p in i)if(/^TV(Db)?/i.test(p)&&i[p])return'TVDb';else if(/^TM(Db)?/i.test(p)&&i[p])return'TMDb';return'IMDb'})(item), + ITEM_URL = (item.href || ''), + FILE_TYPE = (item.tail || 'mp4'), + FILE_PATH = (item.path || ''), + ITEM_ID = ((i, I)=>{for(let p in i)if(RegExp('^'+I,'i').test(p))return i[p]})(item, ID_PROVIDER); + + try { + switch (request.type) { + case 'SEARCH_PLEX': + Search_Plex(request, callback); + return true; + + case 'VIEW_COUCHPOTATO': + Open_CouchPotato(request, callback); + return true; + + case 'PUSH_COUCHPOTATO': + Push_CouchPotato(request, callback); + return true; + + case 'PUSH_RADARR': + Push_Radarr(request, callback); + return true; + + case 'PUSH_SONARR': + Push_Sonarr(request, callback); + return true; + + case 'PUSH_MEDUSA': + Push_Medusa(request, callback); + return true; + + case 'PUSH_WATCHER': + Push_Watcher(request, callback); + return true; + + case 'PUSH_OMBI': + Push_Ombi(request, callback); + return true; + + case 'PUSH_SICKBEARD': + Push_SickBeard(request, callback); + return true; + + case 'OPEN_OPTIONS': + chrome.runtime.openOptionsPage(); + return true; + + case 'SEARCH_FOR': + if(ITEM_TITLE && ITEM_TYPE) + ChangeStatus({ ITEM_ID, ITEM_TITLE, ITEM_TYPE, ID_PROVIDER, ITEM_YEAR, ITEM_URL, FILE_TYPE, FILE_PATH }); + return true; + + case 'SAVE_AS': + chrome.contextMenus.update('W2P-DL', { + title: `Save as "${ ITEM_TITLE } (${ ITEM_YEAR })" (${ FILE_TYPE })` + }); + return true; + + case 'DOWNLOAD_FILE': + let FILE_TITLE = ITEM_TITLE.replace(/\-/g, ' ').replace(/[\s\:]{2,}/g, ' - ').replace(/[^\w\s\-\']+/g, ''); + + // no try/catch, use callback for that + chrome.downloads.download({ + url: item.href, + filename: `${ FILE_TITLE } (${ ITEM_YEAR }).${ FILE_TYPE }`, + saveAs: true + }, id => { + // Error Occured + if(id == undefined || id == null) + chrome.downloads.download({ + url: item.href, + filename: `${ FILE_TITLE } (${ ITEM_YEAR })`, + saveAs: true + }); + }); + return true; + + case 'PLUGIN': + case 'SCRIPT': + case '_INIT_': + case '$INIT$': + case 'FOUND': + case 'GRANT_PERMISSION': + /* These are meant to be handled by plugn.js */ + return false; + + default: + BACKGROUND_TERMINAL.warn(`Unknown event [${ request.type }]`); + return false; + } + } catch (error) { + return callback(String(error)); + } }); // If background.js is already running, ignore the new state // otherwise, use the following to start up if(SessionState === false) { - SessionState = true; - - parentItem = chrome.contextMenus.create({ - id: 'W2P', - title: 'Web to Plex' - }); - - saveItem = chrome.contextMenus.create({ - id: 'W2P-DL', - title: 'Nothing to Save' - }); - - // Standard search engines - for(let array = 'IM TM TV'.split(' '), DL = {}, length = array.length, index = 0, item; index < length; index++) - chrome.contextMenus.create({ - id: 'W2P-' + (item = array[index]), - parentId: parentItem, - title: `Using ${ item }Db`, - type: 'checkbox', - checked: true // implement a way to use the checkboxes? - }); - - // Non-standard search engines - chrome.contextMenus.create({ - id: 'W2P-XX', - parentId: parentItem, - title: `Using best guess`, - type: 'checkbox', - checked: true // implement a way to use the checkboxes? - }); + SessionState = true; + + __context_parent__ = chrome.contextMenus.create({ + id: 'W2P', + title: 'Web to Plex' + }); + + __context_save_element__ = chrome.contextMenus.create({ + id: 'W2P-DL', + title: 'Nothing to Save' + }); + + // Standard search engines + for(let array = 'IM TM TV'.split(' '), DL = {}, length = array.length, index = 0, item; index < length; index++) + chrome.contextMenus.create({ + id: 'W2P-' + (item = array[index]), + parentId: __context_parent__, + title: `Using ${ item }Db`, + type: 'checkbox', + checked: true // implement a way to use the checkboxes? + }); + + // Non-standard search engines + chrome.contextMenus.create({ + id: 'W2P-XX', + parentId: __context_parent__, + title: `Using best guess`, + type: 'checkbox', + checked: true // implement a way to use the checkboxes? + }); + +} +// turn object into URL paramaeters: +// { data: data, ... } => data=data&... +function formify(data) { + let body = []; + for(let key in data) + body.push(`${ key }=${ data[key] }`); + return body.join('&'); } if(chrome.runtime.lastError) - /* Attempt Error Suppression */; + /* Attempt Error Suppression */; diff --git a/src/cloud/__layout__.js b/src/cloud/__layout__.js new file mode 100644 index 0000000..4eef0a5 --- /dev/null +++ b/src/cloud/__layout__.js @@ -0,0 +1,39 @@ +// optional +// "Friendly Name" requires: api|username|password|token +// api - the user's api tokens (external, such as TMDb/OMDb) +// username - the user's usernames (internal, such as Radarr/Sonarr/etc.) +// password - the user's passwords (internal) +// token - the user's tokens (internal) +// Example: "Web to Plex" requires: api, token + +let script = { + // required + "url": "< URL RegExp >", + // Example: *://*.amazon.*/*/video/(detail|buy)/* + // *:// - match any protocol (http, https, etc.) + // *.amazon - match any sub-domain (www, ww5, etc.) + // .* - match any TLD (com, net, org, etc.) + // /* - match any path + // (detail|buy) - match one of the items + + // optional + "ready": () => { /* return a boolean to describe if the page is ready */ }, + + // optional + "timeout": 1000, // if the script fails to complete, retry after ... milliseconds + + // required + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('#title').first, + year = $('#year').first, + image = $('#image').first, + type = script.getType(); // described below + + return { type, title, year, image }; + }, + + // optional | functioanlity only + "getType": () => 'movie' || 'show', +}; diff --git a/src/cloud/__test__.js b/src/cloud/__test__.js new file mode 100644 index 0000000..da0bfb9 --- /dev/null +++ b/src/cloud/__test__.js @@ -0,0 +1,37 @@ +let script = { + // required + "url": "*://ephellon.github.io/web.to.plex/test/*", + // Example: *://*.amazon.com/*/video/(detail|buy)/* + // *:// - match any protocol (http, https, etc.) + // *.amazon.com - match any sub-domain (www, ww5, etc.) + // /* - match any path + // (detail|buy) - match one of the items + + // optional + "ready": () => { + /* return a boolean to describe if the page is ready */ + return !!$('#title').first.textContent.length; + }, + + // optional + "timeout": 1000, // if the script fails to complete, retry after ... milliseconds + + // required + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('#title').first, + year = $('#year').first, + image = $('#poster').first, + type = script.getType(); // described below + + title = title.textContent; + year = +year.textContent; + image = image.src || ''; + + return { type, title, year, image }; + }, + + // optional | functioanlity only + "getType": () => $('#example').first.getAttribute('type'), +}; diff --git a/src/cloud/allocine.js b/src/cloud/allocine.js new file mode 100644 index 0000000..f0dca7b --- /dev/null +++ b/src/cloud/allocine.js @@ -0,0 +1,29 @@ +let script = { + "url": "*://*.allocine.fr/(film|series)/*", + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('.titlebar-title').first, + year = $('.date, .meta-body font').first, + image = $('.thumbnail-img').first, + type = script.getType(); + + if(!title || !year) + return 1000; + + title = title.textContent.trim(); + image = image.src; + + year.textContent.replace(/(\d{4})/, ''); + year = +R.$1; + + return { type, title, year, image }; + }, + + "getType": () => { + let { pathname } = top.location; + + return /\/(film)\//.test(pathname)? 'film': 'show'; + }, +}; diff --git a/src/cloud/amazon.js b/src/cloud/amazon.js new file mode 100644 index 0000000..0779401 --- /dev/null +++ b/src/cloud/amazon.js @@ -0,0 +1,55 @@ +// Web to Plex - Toloka Plugin +// Aurthor(s) - @ephellon (2019) + +/* Minimal Required Layout * + script { + url: string, + init: function => ({ type:string, title:string, year:number|null|undefined }) + } +*/ + +// REQUIRED [script:object]: The script object +let script = { + // REQUIRED [script.url]: this is what you ask Web to Plex access to; currently limited to a single domain + "url": "*://*.amazon.com/*/video/detail/*", + + // PREFERRED [script.ready]: a function to determine that the page is indeed ready + "ready": () => !$('[data-automation-id="imdb-rating-badge"], #most-recent-reviews-content > *:first-child').empty, + + // REQUIRED [script.init]: it will always be fired after the page and Web to Plex have been loaded + // OPTIONAL [ready]: if using script.ready, Web to Plex will pass a boolean of the ready state + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('[data-automation-id="title"], #aiv-content-title, .dv-node-dp-title') + .first.textContent + .replace(/(?:\(.+?\)|(\d+)|\d+\s+seasons?\s+(\d+))\s*$/gi, '') + .trim(), + // REQUIRED [title:string] + // you have access to the exposed "helper.js" file within the extension + year = +( + !(_year = $('[data-automation-id="release-year-badge"], .release-year')).empty? + _year.first.textContent.trim(): + (R.$1 || R.$2 || YEAR) + ), + // PREFERRED [year:number, null, undefined] + image = ( + (_image = $('.av-bgimg__div, div[style*="sgp-catalog-images"]')).empty? + $('.av-fallback-packshot img').src: + getComputedStyle(_image.first).backgroundImage.replace(/[^]*url\((["']?)(.+?)\1\)[^]*/i, '$2') + ), + // the rest of the code is up to you, but should be limited to a layout similar to this + type = script.getType(); + + // REQUIRED [{ type:'movie', 'show'; title:string; year:number }] + // PREFERRED [{ image:string; IMDbID:string; TMDbID:string, number; TVDbID:string, number }] + return { type, title, year, image }; + }, + + // OPTIONAL: the rest of this code is purely for functionality + "getType": () => { + return !$('[data-automation-id*="season"], [class*="season"], [class*="episode"], [class*="series"]').empty? + 'tv': + 'movie' + }, +}; diff --git a/src/cloud/couchpotato.js b/src/cloud/couchpotato.js new file mode 100644 index 0000000..f8f6ddc --- /dev/null +++ b/src/cloud/couchpotato.js @@ -0,0 +1,40 @@ +let script = { + "url": "*://*.couchpotato.life/(movies|shows)/*", + + "ready": () => !$('.media-body .clearfix').empty && $('.media-body .clearfix').first.children.length, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('[itemprop="description"]').first, + year = title.previousElementSibling, + image = $('img[src*="wp-content"]'), + type = script.getType(), + IMDbID = script.getIMDbID(); + + title = title.textContent.trim(); + year = +year.textContent.trim(); + image = image.empty? '': image.first.src; + + return { type, title, year, image, IMDbID }; + }, + + "getType": () => { + let pathname = window.location.pathname; + + return /^\/movies?\//.test(pathname)? + 'movie': + /^\/shows?\//.test(pathname)? + 'show': + null + }, + + "getIMDbID": () => { + let link = $('[href*="imdb.com/title/tt"]'); + + if(!link.empty) + return link.first.href + .replace(/^.*imdb\.com\/title\//, '') + .replace(/\/(?:maindetails\/?)?$/, ''); + }, +}; diff --git a/src/cloud/fandango.js b/src/cloud/fandango.js new file mode 100644 index 0000000..04a1a4e --- /dev/null +++ b/src/cloud/fandango.js @@ -0,0 +1,20 @@ +let script = { + "url": "*://*.fandango.com/[\\w\\-]+/movie-overview", + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('.subnav__title').first, + year = $('.movie-details__release-date').first, + image = $('.movie-details__movie-img').first, + type = 'movie'; + + title = title.textContent.trim().split(/\n+/)[0].trim(); + year = +year.textContent.replace(/.*(\d{4}).*/, '$1').trim(); + image = image.empty? '': image.src; + + title = title.replace(RegExp(`\\s*\\((${ year })\\)`), ''); + + return { type, title, year, image }; + }, +}; diff --git a/src/cloud/flickmetrix.js b/src/cloud/flickmetrix.js new file mode 100644 index 0000000..ab3a18a --- /dev/null +++ b/src/cloud/flickmetrix.js @@ -0,0 +1,61 @@ +let script = { + "url": "*://*.flickmetrix.com/(watchlist|seen|favourites|trash|share|\\?)?", + + "ready": () => $('#loadingOverlay > *').empty, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + if(script.isList()) + return script.processList(ready); + + let element = $('#singleFilm'), type = 'movie'; + + _title = $('.title', film).first; + _year = $('.title + *', film).first; + _image = $('img', film).first; + + let title = _title.textContent.trim(), + year = +_year.textContent.replace(/^\(|\)$/g, '').trim(), + image = _image.src, + IMDbID = script.getIMDbID(element); + + return { type, title, year, image }; + }, + + "getIMDbID": (element) => { + let link = $('[href*="imdb.com/title/tt"]').first; + + if(link) + return link.href.replace(/^.*imdb\.com\/title\//, '').replace(/\/(?:maindetails\/?)?$/, ''); + }, + + "isList": () => $('#singleFilm').empty && !/\bid=\d+\b/i.test(location.search), + + "processList": (ready) => { + let _title, _year, _image, R = RegExp; + + let films = [], list = $('.film'), length = list.length - 1, type = 'movie'; + + list.forEach((element, index, array) => { + _title = $('.title', element).first; + _year = $('.title + *', element).first; + _image = $('img', element).first; + + if(!_title) + return; + + let title = _title.textContent.trim(), + year = +_year.textContent.replace(/^\(|\)$/g, '').trim(), + image = _image.src, + IMDbID = script.getIMDbID(element); + + films.push({ type, title, year, IMDbID }); + }); + + if(!films.length) + return new Notification('error', 'Failed to process list'); + + return films; + }, +}; diff --git a/src/cloud/google.js b/src/cloud/google.js new file mode 100644 index 0000000..43ceb63 --- /dev/null +++ b/src/cloud/google.js @@ -0,0 +1,61 @@ +let SHOW = '[href*="thetvdb.com/"][href*="id="], [href*="thetvdb.com/series/"], [href*="themoviedb.org/tv/"], [href*="imdb.com/title/tt"][href$="externalsites"]', + FILM = '[href*="themoviedb.org/tv/"], [href*="imdb.com/title/tt"]'; + // FILM = '#media_result_group, ...' + +let script = { + "url": "*://www.google.com/search", + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let type = script.getType(); + + if(type == 'movie') { + let _type = $('.kno-ecr-pt + *').first; // in case a tv show is incorrectly identified + + if(_type) { + type = _type.textContent; + + type = /\b(tv|show|series)\b/i.test(type)? 'show': /\b(movie|film|cinema|(?:\d+h\s+)?\d+m)\b/i.test(type)? 'movie': 'error'; + _year = (type == 'show'? $('.kno-fv').first || _year: _year) || { textContent: '' }; + } + + _title = $('.kno-ecr-pt').first; + _year = $('.kno-fb-ctx:not([data-local-attribute]) span').first; + _image = $('#media_result_group img').first; + } else if(type == 'show') { + _title = $(SHOW).first.querySelector('*'); + _year = { textContent: '' }; + _image = { src: '' }; + } else if(type == 'error') { + return null; + } + + (_year.textContent + '').replace(/(\d{4})/); + + let year = +R.$1, + title = _title.textContent.replace((type == 'movie'? /^(.+)$/: /(.+)(?:(?:\:\s*series\s+info|\-\s*(?:all\s+episodes|season)).+)$/i), '$1').trim(), + image = (_image || {}).src; + + year = year > 999? year: 0; + + let IMDbID = script.getIMDbID(); + + return { type, title, year, image, IMDbID }; + }, + + "getIMDbID": () => { + let link = $('a._hvg[href*="imdb.com/title/tt"]').first; + + if(link) + return link.href.replace(/.*(tt\d+).*/, '$1'); + }, + + "getType": () => ( + !$(SHOW).empty? + 'show': + !$(FILM).empty? + 'movie': + 'error' + ), +}; diff --git a/src/cloud/google.play.js b/src/cloud/google.play.js new file mode 100644 index 0000000..603e989 --- /dev/null +++ b/src/cloud/google.play.js @@ -0,0 +1,24 @@ +let script = { + "url": "*://play.google.com/store/(movies|tv)/details/*", + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let type = script.getType(), + title = $('h1').first, + year = $(`h1 ~ div span:${ type == 'movie'? 'first': 'last' }-of-type`).first, + image = $('img[alt="cover art" i]').first; + + title = title.textContent.replace(/\s*\(\s*(\d{4})\s*\).*?$/, '').trim(); + year = +(year.textContent || R.$1).replace(/^.*?(\d{4})/, '$1').trim(); + image = (image || {}).src; + + return { type, title, year, image }; + }, + + "getType": () => ( + location.pathname.startsWith('/store/movies')? + 'movie': + 'show' + ), +}; diff --git a/src/cloud/gostream.js b/src/cloud/gostream.js new file mode 100644 index 0000000..1aaf12b --- /dev/null +++ b/src/cloud/gostream.js @@ -0,0 +1,22 @@ +let script = { + "url": "*://*.gostream.site/(?!genre|most-viewed|top-imdb|contact)", + + "ready": () => { let e = $('.movieplay iframe, .desc iframe'); return e.empty? false: e.first.src != '' }, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('[itemprop="name"]:not(meta)').first, + year = $('.mvic-desc [href*="year/"]').first, + image = $('.hiddenz, [itemprop="image"]').first, + type = 'movie'; + + Notify('update', 'Select the OL/VH server'); + + title = title.textContent.trim(); + year = +(year? year.textContent.trim(): 0); + image = (image? image.src: null); + + return { type, title, year, image }; + }, +}; diff --git a/src/cloud/hulu.js b/src/cloud/hulu.js new file mode 100644 index 0000000..89a8081 --- /dev/null +++ b/src/cloud/hulu.js @@ -0,0 +1,48 @@ +let script = { + "url": "*://*.hulu.com/(watch|series|movie)/*", + + "ready": () => !$('[class$="__meta"]').empty, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + let { pathname } = top.location; + let type, title, year, image; + + if(/^\/(series|movie)\//.test(pathname)) { + type = R.$1; + title = $('[class~="masthead__title"i]').first; + year = $('[class~="masthead__meta"i]').child(type == 'series'? 4: 3); + image = $('[class~="masthead__artwork"i]').first; + + title = title.textContent; + year = +year.textContent; + type = /\b(tv|show|season|series)\b/i.test(type)? 'show': 'movie'; + image = image? image.src: null; + } else { + title = $('[class$="__second-line"]').first; + year = (new Date).getFullYear(); + type = script.getType(); + + title = title.textContent; + } + + if(!title) + return 5000; + + return { type, title, year, image }; + }, + + "getType": () => { + let { pathname } = top.location; + + if(/^\/series\//.test(pathname)) { + return 'show'; + } else { + let tl = $('[class$="__third-line"]').first; + + return /^\s*$/.test(tl.textContent)? + 'movie': + 'show'; + } + }, +}; diff --git a/src/cloud/imdb.js b/src/cloud/imdb.js new file mode 100644 index 0000000..3e8a624 --- /dev/null +++ b/src/cloud/imdb.js @@ -0,0 +1,119 @@ +let script = { + "url": "*://*.imdb.com/(title|list)/(tt|ls)\\d+/", + + "ready": () => !$('#servertime').empty, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let type = script.getType(), + IMDbID = script.getIMDbID(), + title, year, image; + + let usa = /\b(USA?|United\s+States)\b/i, + date, country, reldate, regdate, alttitle, options; + + switch(type) { + case 'movie': + title = $('.originalTitle, .title_wrapper h1'); + alttitle = title.first; + reldate = $('.title_wrapper [href*="/releaseinfo"]').first; + year = $('.title_wrapper #titleYear').first; + image = $('img[alt$="poster"i]').first; + + // TODO: Less risky way to accompilsh this? + title = title.last.childNodes[0].textContent.trim(); + alttitle = (alttitle == title? title: alttitle.childNodes[0].textContent.trim()); + title = usa.test(country)? title: alttitle; + country = reldate.textContent.replace(/[^]+\((\w+)\)[^]*?$/, '$1'); + year = +script.clean(year.textContent); + image = (image || {}).src; + options = { type, title, alttitle, year, image }; + break; + + case 'show': + title = $('.originalTitle, .title_wrapper h1'); + alttitle = title.first; + reldate = $('.title_wrapper [href*="/releaseinfo"]').first; + date = $('title').first.textContent.trim(); + regdate = date.match(/Series\s*\(?(\d{4})(?:[^\)]+\))?/i); + image = $('img[alt$="poster"i]').first; + + // TODO: Less risky way to accompilsh this? + title = title.last.textContent.trim(); + alttitle = (alttitle == title? title: alttitle.childNodes[0].textContent.trim()); + title = usa.test(country)? title: alttitle; + country = reldate.textContent.replace(/[^]+\((\w+)\)[^]*?$/, '$1'); + year = parseInt(regdate[1]); + image = (image || {}).src; + options = { type, title, alttitle, year, image }; + break; + + case 'list': + let items = $('#main .lister-item'); + + options = []; + + if(!/[\?\&]mode=simple\b/i.test(top.location.search)) + top.open(location.href.replace(/([\?\&]|\/$)(?:mode=\w+&*)?/, '$1mode=simple&'), '_self'); + + items.forEach(element => { + let option = script.process(element); + + if(option) + options.push(option); + }); + break; + + default: return null; + } + + return options; + }, + + "getType": () => { + let tag = $('meta[property="og:type"]').first, + type = 'error'; + + if(tag) { + switch(tag.content) { + case 'video.movie': + type = 'movie'; + break; + + case 'video.tv_show': + type = 'show'; + break; + }; + } else if(top.location.pathname.startsWith('/list/')) { + type = 'list'; + } + + return type; + }, + + "getIMDbID": () => { + let tag = $('meta[property="pageId"]'); + + return tag? tag.content: null; + }, + + "process": (element) => { + let title = $('.col-title a', element).first, + year = $('.col-title a + *', element).first, + image = $('img.loadlate, img[data-tconst]', element).first, + IMDbID = title.href.replace(/^[^]*\/(tt\d+)\b[^]*$/, '$1'), + type; + + title = title.textContent.trim(); + year = script.clean(year.textContent); + image = image.src; + type = (/[\-\u2010-\u2015]/.test(year)? 'show': 'movie'); + + year = +year; + + return { type, title, year, image, IMDbID }; + }, + + "clean": year => (year + '').replace(/^\(|\)$/g, '').trim(), +}; diff --git a/src/cloud/itunes.js b/src/cloud/itunes.js new file mode 100644 index 0000000..45f4cc8 --- /dev/null +++ b/src/cloud/itunes.js @@ -0,0 +1,47 @@ +let script = { + "url": "", + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title, year, image, type = script.getType(); + + switch(type) { + case 'movie': + title = $('[class*="movie-header__title"i]').first.textContent; + year = +$('[datetime]').first.textContent || title.replace(RegExp(`[^]*\\((${ year })\\)[^]*`), '$1'); + image = ($('[class*="product"] ~ * picture img').first || {}).src; + + title = title.replace(RegExp(`\\s*\\(${ year }\\)`), ''); + break; + + case 'tv': + title = $('h1[itemprop="name"], h1').first.textContent.replace(/\s*\((\d+)\)\s*/, '').trim(); + year = +$('.release-date > *:last-child').first.textContent.replace(/[^]*(\d{4})[^]*?$/g, '$1').trim(); + image = $('[class*="product"] ~ * picture img').first.src; + + title = title.replace(RegExp(`\\s*\\(${ year }\\)`), ''); + break; + + default: + /* Error */ + return {}; + } + + setTimeout(script.adjustButton, 1000); + + return { type, title, year, image }; + }, + + "getType": () => { + return /(\/\w+)?\/tv-season\//.test(top.location.pathname)? + 'tv': + 'movie' + }, + + "adjustButton": () => { + let button = $('.web-to-plex-button').first; + + button.attributes.style.value += '; box-sizing: border-box !important; font-size: 16px !important; line-height: normal !important;'; + }, +}; diff --git a/src/cloud/justwatch.js b/src/cloud/justwatch.js new file mode 100644 index 0000000..35cedeb --- /dev/null +++ b/src/cloud/justwatch.js @@ -0,0 +1,31 @@ +let script = { + "url": "*://*.justwatch.com/(\\w{2})/(tv(?:-show)|movie)/*", + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('.title-block').first, + year = $('.title-block .text-muted').first, + image = $('.title-poster__image').first, + type = script.getType(); + + if(!title || !year) + return 1000; + + year = year.textContent; + title = title.firstElementChild.firstChild.textContent.trim(); + year = +year.replace(/\D+/g, ''); + image = image.src; + + return { type, title, year, image }; + }, + + "getType": () => { + let { pathname } = top.location; + + if(/^\/tv(-show)?\//.test(pathname)) + return 'show'; + else + return 'movie'; + }, +}; diff --git a/src/cloud/letterboxd.js b/src/cloud/letterboxd.js new file mode 100644 index 0000000..d8c1ac7 --- /dev/null +++ b/src/cloud/letterboxd.js @@ -0,0 +1,76 @@ +let script = { + "url": "*://*.letterboxd.com/(film|list)/", + + "ready": () => (script.getType('list')? true: !$('.js-watch-panel').empty), + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title, year, image, type = script.getType(), IMDbID; + + switch(type) { + case 'movie': + title = $('.headline-1[itemprop="name"]').first.textContent.trim(); + year = +$('small[itemprop="datePublished"]').first.textContent.trim(); + image = ($('.image').first || {}).src; + IMDbID = script.getIMDbID(type); + + return { type, title, year, image, IMDbID }; + break; + + case 'list': + let items = $('.poster-list .poster-container'), + options = []; + + items.forEach((element, index, array) => { + let option = script.process(element); + + if(option) + options.push(option); + }); + + return options; + break; + + default: + /* Error */ + return {}; + } + }, + + "getType": (suspectedType) => { + let type = /^\/(film)\//i.test(top.location.pathname)? 'movie': 'list'; + + if(suspectedType) + return type == suspectedType; + + return type; + }, + + "getIMDbID": (type) => { + if(type == 'movie') { + let link = $( + '.track-event[href*="imdb.com/title/tt"i]' + ); + + if(!link.empty) { + link = link.first.href.replace(/^.*imdb\.com\/title\//i, ''); + + return link.replace(/\/(?:maindetails\/?)?$/, ''); + } + } + }, + + "process": (element) => { + let title = $('.frame-title', element).first, + image = $('img', element).first, + type = 'movie', + year; + + title = title.textContent.replace(/\((\d+)\)/, '').trim(); + year = +RegExp.$1; + image = image.src; + + return { type, title, year, image }; + }, +}; diff --git a/src/cloud/metacritic.js b/src/cloud/metacritic.js new file mode 100644 index 0000000..d61d41d --- /dev/null +++ b/src/cloud/metacritic.js @@ -0,0 +1,49 @@ +let script = { + "url": "*://*.metacritic.com/(movie|tv|list)/*", + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title, year, image, + type = script.getType(); + + switch(type) { + case 'tv': + case 'movie': + title = $('.product_page_title > *, .product_title').first; + year = $('.product_page_title > .release_year, .product_data .release_data').first; + image = $('.summary_img').first; + + title = title.textContent.replace(/\s+/g, ' ').trim(); + year = +year.textContent.replace(/\s+/g, ' ').replace(/.*(\d{4}).*$/, '$1').trim(); + image = (image || {}).src; + + type = type == 'tv'? 'show': type; + + return { type, title, year, image }; + break; + + case 'list': + /* Not yet implemented */ + break; + + default: + /* Error */ + return {}; + break; + } + }, + + "getType": () => { + /^\/(movie|tv|list)\//.test(top.location.pathname); + + let type = RegExp.$1; + + return type; + }, + + "process": (element) => { + /* Not implemented... Metacritic has too much sh*t loading to even try to open a console */ + /* Targeted for v5/v6 */ + }, +}; diff --git a/src/cloud/moviemeter.js b/src/cloud/moviemeter.js new file mode 100644 index 0000000..84d0080 --- /dev/null +++ b/src/cloud/moviemeter.js @@ -0,0 +1,34 @@ +let script = { + "url": "*://*.moviemeter.nl/film/\\d+", + + "ready": () => !$('.rating + p font').empty, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('.details span').first, + year = $('.details *').first, + image = $('.poster').first, + type = script.getType(); + + if(!title || !year) + return 1000; + + year = year.lastChild.textContent; + title = title.textContent.replace(year, '').trim(); + year = +year.replace(/\D+/g, ''); + image = image.src; + + return { type, title, year, image }; + }, + + "getType": () => { + let time = $('.rating + p font').last; + + time = time.textContent; + + if(/(series|show)/.test(time)) + return 'show'; + return 'film'; + }, +}; diff --git a/src/cloud/movieo.js b/src/cloud/movieo.js new file mode 100644 index 0000000..dbf9601 --- /dev/null +++ b/src/cloud/movieo.js @@ -0,0 +1,75 @@ +let script = { + "url": "*://*.movieo.me/*", + + "ready": () => !$('.share-box, .zopim').empty, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title, year, image, IMDbID, + type = script.getType(); + + switch(type) { + case 'movie': + title = $('#doc_title').first; + year = $('meta[itemprop="datePublished"i]').first; + image = $('img.poster').first; + + title = title.dataset.title.trim(); + year = +year.content.slice(0, 4); + image = (image || {}).src; + IMDbID = script.getIMDbID(); + break; + + case 'list': + let items = $('[data-title][data-id]'), + options = []; + + items.forEach((element, index, array) => { + let option = script.process(element); + + if(option) + options.push(option); + }); + + return options; + break; + + default: + /* Error */ + return {}; + break; + } + + return { type, title, year, image }; + }, + + "getType": () => { + let type = /\/(black|seen|watch)?lists?\//i.test(top.location.pathname)? + 'list': + 'movie'; + + return type; + }, + + "getIMDbID": () => { + let link = $( + '.tt-parent[href*="imdb.com/title/tt"i]' + ).first; + + if(link) + return link.href.replace(/^[^]*\/title\//i, ''); + }, + + "process": (element) => { + let title = $('.title', element).first, + image = $('.poster-cont', element).first, + year, type = 'movie'; + + title = title.textContent.trim().replace(/\s*\((\d{4})\)/, ''); + year = +RegExp.$1; + image = image.getAttribute('data-src'); + + return { type, title, year, image }; + }, +}; diff --git a/src/cloud/netflix.js b/src/cloud/netflix.js new file mode 100644 index 0000000..1a22407 --- /dev/null +++ b/src/cloud/netflix.js @@ -0,0 +1,28 @@ +let script = { + "url": "*://*.netflix.com/watch/\\d+", + + "ready": () => { + let element = $('[class$="__time"]').first; + + return element && !/^([0:]+|null|undefined)?$/.test(element.textContent); + }, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('.video-title h4').first, + year = 0, + image = '', + type = script.getType(); + + title = title.textContent; + + return { type, title, year, image }; + }, + + "getType": () => { + let element = $('[class*="playerEpisodes"]').first; + + return !!element? 'show': 'movie'; + }, +}; diff --git a/src/cloud/plex.js b/src/cloud/plex.js new file mode 100644 index 0000000..4a1f09d --- /dev/null +++ b/src/cloud/plex.js @@ -0,0 +1,36 @@ +let script = { + "url": "*://app.plex.tv/desktop#!/server/([a-f\\d]+)/(details|list)\\?*", + + "ready": () => $('.loading').empty, + + "timeout": 5000, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('[data-qa-id$="maintitle"i] *').first, + year = $('[data-qa-id$="secondtitle"i] *').first, + type = script.getType(); + + if(!title || !year || type == 'error') + return 5000; + + title = title.textContent; + year = year.textContent; + + year = +(year || YEAR); + + return { type, title, year }; + }, + + "getType": () => { + let cell = $('[data-qa-id$="celltitle"i]').first; + + if(!cell) + return 'error'; + + if(/seasons?/i.test(cell.textContent)) + return 'show'; + return 'movie'; + }, +}; diff --git a/src/cloud/plugin.indomovietv.js b/src/cloud/plugin.indomovietv.js new file mode 100644 index 0000000..63ea78d --- /dev/null +++ b/src/cloud/plugin.indomovietv.js @@ -0,0 +1,59 @@ +let plugin = { + "url": "*://*.indomovietv.*/(?!tag|$)", + // TLD changes often: net, org + + "ready": () => !$('[itemprop="name"i]:not(meta), [itemprop="datePublished"i]').empty, + + "timeout": 1000, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('[itemprop="name"i]:not(meta)').first, + year = $('[itemprop="datePublished"i]').first, + image = $('[itemprop="image"i]').first, + type = 'movie'; + + title = title.textContent; + year = +year.textContent.replace(/[^]*(\d{4})[^]*/, '$1'); + image = image.src; + + // auto-prompt downloading for the user + let links = $('[class~="idtabs"i] [href^="#div"i]'); + + if(links.length > 1) { + OLOAD_EVENTS.push(setTimeout( + () => Notify('update', 'Finding download links...', 3000), + 500 + )); + + links.forEach((link, index, array) => OLOAD_EVENTS.push(setTimeout( + () => { + link.click(); + + if(index == links.length -1) + OLOAD_EVENTS.push(setTimeout( + () => Notify('update', 'No download links found'), + 7000 + )); + }, + index * 4500 + ))); + } + + return { type, title, year, image }; + }, +}, + OLOAD_EVENTS = []; + +top.addEventListener('message', request => { + try { + request = request.data; + + if(request) + if(request.from == 'oload' || request.found == true) + OLOAD_EVENTS.forEach(timeout => clearTimeout(timeout)); + } catch(error) { + throw error; + } +}); diff --git a/src/cloud/plugin.kitsu.js b/src/cloud/plugin.kitsu.js new file mode 100644 index 0000000..c55730f --- /dev/null +++ b/src/cloud/plugin.kitsu.js @@ -0,0 +1,36 @@ +// Web to Plex - Kitsu Plugin +// Aurthor(s) - @ephellon (2019) +let plugin = { + "url": "*://*.kitsu.io/anime/*", + + "ready": () => !$('img[data-src][src]').empty, + + "timeout": 1000, + + "init": () => { + let _title = /^\s*(?:english|romanized)\s+(.+)\s*$/i, + _year = /^\s*aired\s+.+(\d{4})(?:\s+to.+)?\s*$/i; + + let title = $('.media--information li').filter(e => _title.test(e.textContent))[0], + year = $('.media--information li').filter(e => _year.test(e.textContent))[0], + image = $('.media-poster img').first, + type = plugin.getType(); + + title = title.textContent.replace(_title, '$1'); + year = +year.textContent.replace(_year, '$1'); + image = image.src; + + return { + type, + title, + year, + image + }; + }, + + "getType": () => { + $('.media--information li').filter(e => /^\s*type\s+(movie|tv([\s\-]?show)?)\s*$/i.test(e.textContent)); + + return /tv/i.test(RegExp.$1)? 'show': 'movie'; + }, +}; diff --git a/src/cloud/plugin.myanimelist.js b/src/cloud/plugin.myanimelist.js new file mode 100644 index 0000000..aa36851 --- /dev/null +++ b/src/cloud/plugin.myanimelist.js @@ -0,0 +1,29 @@ +// Web to Plex - My Anime List Plugin +// Aurthor(s) - @ephellon (2018) + +let plugin = { + "url": "*://*.myanimelist.net/anime/\\d+/*", + + "init": () => { + let title = document.queryBy('table h2:nth-of-type(1) + *') + .first.textContent.replace(/^[^\:]+:/, '') + .trim(), + type = document.queryBy('table h2:nth-of-type(2) + *') + .first.textContent.trim() + .toLowerCase() + .split(/\s+/) + .reverse()[0], + year = +(document.queryBy('table h2:nth-of-type(2) ~ .spaceit ~ .spaceit') + .first.textContent.trim() + .replace(/[^]*(\d{4})[^]*/, '$1')), + image = document.queryBy('table img') + .first.src; + + return { + type, + title, + year, + image + }; + }, +}; diff --git a/src/cloud/plugin.myshows.js b/src/cloud/plugin.myshows.js new file mode 100644 index 0000000..f3946ac --- /dev/null +++ b/src/cloud/plugin.myshows.js @@ -0,0 +1,29 @@ +// Web to Plex - My Shows Plugin +// Aurthor(s) - @enchained (2018) + +let plugin = { + "url": "*://*.myshows.me/view/\\d+/*", + + "init": () => { + let specific = /\/\/(\w{2})\./.test(location.origin); + + let title = ( + specific ? + document.queryBy('[itemprop="name"]').first.textContent: + document.queryBy('main > h1').first.textContent + ) + .trim(), + year = +(document.queryBy('div.clear > p.flat') + .first.textContent.trim() + .replace(/[^]*?(\d{4})[^]*/, '$1')), + IMDbID = document.queryBy('[href*="/title/tt"]') + .first.href.replace(/[^]*(tt\d+)[^]*/, '$1'); + + return { + type: 'show', + title, + year, + IMDbID + }; + }, +}; diff --git a/src/cloud/plugin.redbox.js b/src/cloud/plugin.redbox.js new file mode 100644 index 0000000..1724a3f --- /dev/null +++ b/src/cloud/plugin.redbox.js @@ -0,0 +1,28 @@ +let plugin = { + "url": "*://*.redbox.com/(ondemand-)?(movies|tvshows)/(?!featured|$)", + + "ready": () => !$('[data-test-id$="-name"i]').empty, + + "timeout": 1000, + + "init": (ready) => { + let R = RegExp; + + let title = $('[data-test-id$="-name"i]').first, + year = $('[data-test-id$="-info"i]').first, + image = $('[data-test-id$="-img"i]').first, + type = plugin.getType(); + + title = title.textContent.replace(/\s*\((\d{4})\)/, ''); + year = +(R.$1 || year.textContent.split(/\s*\|\s*/)[1]); + image = image.src; + + return { type, title, year, image }; + }, + + "getType": () => { + return /\bmovies\b/.test(location.pathname)? + 'movie': + 'show'; + }, +}; diff --git a/src/cloud/plugin.shanaproject.js b/src/cloud/plugin.shanaproject.js new file mode 100644 index 0000000..2d47688 --- /dev/null +++ b/src/cloud/plugin.shanaproject.js @@ -0,0 +1,23 @@ +// Web to Plex - Shana Project Plugin +// Aurthor(s) - @ephellon (2018) +let plugin = { + "url": "*://*.shanaproject.com/series/\\d+", + + "init": () => { + let title = $('.overview i, #header_big .header_info_block') + .first.textContent.trim(), + year = +$('#header_big .header_info_block + *') + .first.textContent.trim() + .replace(/[^]*(\d{4})[^]*/m, '$1'), + image = $('#header_big .header_display_box') + .first.style['background-image'].trim() + .replace(/url\((.+)\)/i, '$1'); + + return { + type: 'show', + title, + year, + image + }; + }, +}; diff --git a/src/cloud/plugin.toloka.js b/src/cloud/plugin.toloka.js new file mode 100644 index 0000000..db5f6dd --- /dev/null +++ b/src/cloud/plugin.toloka.js @@ -0,0 +1,53 @@ +// Web to Plex - Toloka Plugin +// Aurthor(s) - @chmez (2017) +/* Minimal Required Layout * + plugin { + url: string, + init: function => ({ type:string, title:string, year:number|null|undefined }) + } +*/ +// REQUIRED [plugin:object]: The plugin object +let plugin = { + // REQUIRED [plugin.url]: this is what you ask Web to Plex access to; currently limited to a single domain + "url": "*://*.toloka.to/*", + + // REQUIRED [plugin.init]: this is what Web to Plex will call on when the url is detected + // it will always be fired after the page and Web to Plex have been loaded + "init": () => { + let title = document.queryBy('.maintitle') + .first.textContent.replace(/^.+\/(.+?)\(([\d]{4})\)\s*$/, '$1') + .trim(), + // REQUIRED [title:string] + // you have access to the exposed "helper.js" file within the extension + + year = +RegExp.$2, + // PREFERRED [year:number, null, undefined] + + image = document.queryBy('.postbody img') + .first.src, + // OPTIONAL [image:string] + + IMDbID = plugin.getID(); + + // the rest of the code is up to you, but should be limited to a layout similar to this + // REQUIRED [{ type:'movie', 'show'; title:string; year:number }] + // PREFERRED [{ image:string; IMDbID:string; TMDbID:string, number; TVDbID:string, number }] + return { + type: 'movie', + title, + year, + image, + IMDbID + }; + }, + + // OPTIONAL: the rest of this code is purely for functionality + "getID": () => { + let links = document.queryBy('.postlink'), + regex = /^https?\:\/\/(?:w{3}\.)?imdb\.com\/title\/(tt\d+)/i; + + for(let link in links) + if(regex.test(links[link])) + return RegExp.$1; + } +}; diff --git a/src/cloud/rottentomatoes.js b/src/cloud/rottentomatoes.js new file mode 100644 index 0000000..95075a3 --- /dev/null +++ b/src/cloud/rottentomatoes.js @@ -0,0 +1,85 @@ +let script = { + "url": "*://*.rottentomatoes.com/([mt]|browse)/*", + + "ready": () => { + let element = $('#reviews').first; + + return !!element; + }, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title, type, year, image; + + type = script.getType(); + + switch(type) { + case 'movie': + case 'show': + title = $('.playButton + .title, [itemprop="name"], [class*="wrap__title" i]').first; + year = $('time').first; + image = $('[class*="posterimage" i]').first; + + if(!title) + return 1000; + + title = title.textContent.trim().replace(/(.+)\:[^]*$/, type == 'movie'? '$&': '$1'); + year = +year.textContent.replace(/[^]*(\d{4})/, '').trim(); + image = (image || {}).srcset; + + if(image) + image = image.replace(/([^\s]+)[^]*/, '$1'); + + return { type, title, year, image }; + break; + + case 'list': + let options, elements = $('.mb-movie'); + + elements.forEach((element, index, array) => { + let option = script.process(element); + + if(option) + options.push(option); + }); + + return options; + break; + + default: + return 1000; + break; + } + }, + + "getType": () => { + let { pathname } = top.location; + + return (/^\/browse\/i/.test(pathname))? + 'list': + (/^\/m/.test(pathname))? + 'movie': + (/^\/t/.test(pathname))? + 'show': + 'error'; + }, + + "process": (element) => { + let title = $('.movieTitle').first, + image = $('.poster').first, + type = $('[href^="/m/"], [href^="/t/"]').first; + + title = title.textContent.trim(); + image = image.src; + type = /\/([mt])\//i.test(type.href)? RegExp.$1 == 'm'? 'movie': 'show': null; + + if(!type) + return {}; + + if(type == 'show') + title = title.replace(/\s*\:\s*seasons?\s+\d+\s*/i, ''); + + return { type, title, image }; + }, +}; diff --git a/src/cloud/tmdb.js b/src/cloud/tmdb.js new file mode 100644 index 0000000..d4d7163 --- /dev/null +++ b/src/cloud/tmdb.js @@ -0,0 +1,79 @@ +let script = { + "url": "*://*.themoviedb.org/(movie|tv)/\\d+", + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let type = script.getType(), + TMDbID = script.getTMDbID(), + title, year, image; + + let options; + + switch(type) { + case 'movie': + case 'tv': + title = $('.title > span > *:not(.release_date)').first; + year = $('.title .release_date').first; + image = $('img.poster').first; + + title = title.textContent.trim(); + year = +year.textContent.replace(/\(|\)/g, '').trim(); + image = (image || {}).src; + + if(type != 'movie') + type = 'show'; + + options = { type, title, year, image, TMDbID }; + break; + + case 'list': + let items = $('.item.card'); + + options = []; + + items.forEach(element => { + let option = script.process(element); + + if(option) + options.push(option); + }); + break; + + default: return null; + } + + return options; + }, + + "getType": () => { + let { pathname } = top.location; + + return (/\/(movie|tv)\/\d+/.test(pathname))? + RegExp.$1: + (/(^\/discover\/|\/(movie|tv)\/([^\d]+|\B))/i.test(pathname))? + 'list': + 'error'; + }, + + "getTMDbID": () => { + return +top.location.pathname.replace(/\/(?:movie|tv)\/(\d+).*/, '$1'); + }, + + "process": (element) => { + let title = $('.title').first, + year = $('.title + *').first, + image = $('.poster').first, + type = title.id.split('_'), + TMDbID = +type[1]; + + title = title.textContent.trim(); + year = year.textContent; + image = image.src; + type = (type[0] == 'movie'? 'movie': 'show'); + + year = +year; + + return { type, title, year, image, TMDbID }; + }, +}; diff --git a/src/cloud/trakt.js b/src/cloud/trakt.js new file mode 100644 index 0000000..77c531d --- /dev/null +++ b/src/cloud/trakt.js @@ -0,0 +1,104 @@ +/** TODO + - re-enable list functionality (fix it) +**/ + +let script = { + "url": "*://*.trakt.tv/(movie|show)s/*", + + "ready": () => !$('#info-wrapper ul.external, .format-date').empty, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let type = script.getType(), + IMDbID, TMDbID, TVDbID, + title, year, image, options; + + switch(type) { + case 'movie': + case 'show': + title = $('.mobile-title').first; + year = $('.mobile-title .year').first; + image = $('.poster img.real[alt="poster"i]').first; + IMDbID = script.getIMDbID(); + TMDbID = script.getTMDbID(); + TVDbID = script.getTVDbID(); + + if(!IMDbID && !TMDbID && !TVDbID) + return 5000; + + title = title.textContent.replace(/(.+)(\d{4}).*?$/, '$1').replace(/\s*\:\s*Season.*$/i, '').trim(); + year = +(R.$2 || year.textContent).trim(); + image = (image || {}).src; + + options = { type, title, year, image, IMDbID, TMDbID, TVDbID }; + break; + + case 'list': + let items = $('*'); + + options = []; + + items.forEach((element, index, array) => { + let option = script.process(element, items); + + if(option) + options.push(option); + }); + break; + + default: + return null; + } + + return options; + }, + + "getType": () => { + let { pathname } = top.location; + + return ( + // /^\/(dashboard|calendars|people|search|(?:movie|show)s?\/(?:trending|popular|watched|collected|anticipated|boxoffice)|$)/i.test(pathname)? + // 'list': + /^\/(movie|show)s\//i.test(pathname)? + RegExp.$1: + 'error' + ) + }, + + "getIMDbID": () => { + let link = $( + // HTTPS and HTTP + '[href*="imdb.com/title/tt"]' + ).first; + + if(link) + return link.href.replace(/^.*?imdb\.com\/.+\b(tt\d+)\b/, '$1'); + }, + + "getTMDbID": () => { + let link = $( + // HTTPS and HTTP + '[href*="themoviedb.org/"]' + ).first; + + if(link) + return link.href.replace(/^.*?themoviedb.org\/(?:movie|tv|shows?|series)\/(\d+).*?$/, '$1'); + }, + + "getTVDbID": () => { + let link = $( + // HTTPS and HTTP + '[href*="thetvdb.com/"]' + ).first; + + if(link) + return link.href.replace(/^.*?thetvdb.com\/.+\/(\d+)\b.*?$/, '$1'); + }, + + "process": (element, elements) => { + let type, title, year; + + return { type, title, year }; + }, +}; diff --git a/src/cloud/tubi.js b/src/cloud/tubi.js new file mode 100644 index 0000000..7ede144 --- /dev/null +++ b/src/cloud/tubi.js @@ -0,0 +1,22 @@ +let script = { + "url": "*://*.tubitv.com/(movies|series)/\\d+/*", + + "timeout": 1000, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('._1mbQP').first, + year = $('._3BhXb').first, + image = $('._2TykB').first, + type = script.getType(); // described below + + title = title.textContent.trim(); + year = +year.textContent.replace(/[^]*\((\d+)\)[^]*/g, '$1').trim(); + image = image.getAttribute('style').replace(/[^]+url\('([^]+?)'\)/, '$1'); + + return { type, title, year, image }; + }, + + "getType": () => (/^\/movies?/.test(top.location.pathname)? 'movie': 'show'), +}; diff --git a/src/cloud/tvdb.js b/src/cloud/tvdb.js new file mode 100644 index 0000000..ba29377 --- /dev/null +++ b/src/cloud/tvdb.js @@ -0,0 +1,44 @@ +let script = { + "url": "*://*.thetvdb.com/series/*", + + "ready": () => !$('#series_basic_info').empty, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('#series_title, .translated_title').first, + image = $('img[src*="/posters/"]').first, + type = 'show', + TVDbID = script.getTVDbID(), + Db = {}, year; + + title = title.textContent.trim(); + image = (image || {}).src; + + $('#series_basic_info').first.textContent + .replace(/^\s+|\s+$/g, '') + .replace(/^\s+$/gm, '') + .replace(/^\s+(\S)/gm, '$1') + .split(RegExp(`\\n*\\n*`)) + .forEach(value => { + value = value.split(/\n+/, 2); + + let n = value[0], v = value[1]; + + n = n.replace(/^([\w\s]+).*$/, '$1').replace(/\s+/g, '_').toLowerCase(); + + Db[n] = /,/.test(v)? v.split(/\s*,\s*/): v; + }); + + year = +(((Db.first_aired || YEAR) + '').slice(0, 4)); + + return { type, title, year, image, TVDbID }; + }, + + "getTVDbID": () => { + let { pathname } = top.location; + + if(/\/series\/(\d+)/.test(pathname)) + return RegExp.$1; + }, +}; diff --git a/src/cloud/tvmaze.js b/src/cloud/tvmaze.js new file mode 100644 index 0000000..3332237 --- /dev/null +++ b/src/cloud/tvmaze.js @@ -0,0 +1,27 @@ +let script = { + "url": "*://*.tvmaze.com/shows/*", + + "ready": () => !$('#general-info-panel .rateit').empty, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('header.columns > h1').first, + year = $('#year').first, + image = $('figure img').first, + type = 'show', + TVDbID = script.getTVDbID(); + + title = title.textContent.trim(); + year = +year.textContent.replace(/\((\d+).+\)/, '$1'); + image = (image || {}).src; + + return { type, title, year, image, TVDbID }; + }, + + "getTVDbID": () => { + let { pathname } = top.location; + + return pathname.replace(/\/shows\/(\d+).*/, '$1'); + }, +}; diff --git a/src/cloud/verizon.js b/src/cloud/verizon.js new file mode 100644 index 0000000..8a89d99 --- /dev/null +++ b/src/cloud/verizon.js @@ -0,0 +1,58 @@ +let script = { + "url": "*://*.verizon.com/*/(movie|show)s?/*", + + "ready": () => !$('.container .btn-with-play, .moredetails, .more-like').empty, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let image = $('.cover img').first, + type = script.getType(), + title, year; + + if(script.ondemand) { + if(type == 'movie') { + title = $('.detail *').first; + year = $('.rating *').first; + } else if(type == 'show') { + title = { textContent: top.location.pathname.replace(/\/ondemand\/tvshows?\/([^\/]+?)\/.*/i) }; + year = $('#showDetails > * > *:nth-child(4) *:last-child').first; + + title.textContent = decodeURL(title.textContent).toCpas(); + } else { + return null; + } + } else if(script.watch) { + title = $('[class*="title__"]').first; + year = $('[class*="subtitle__"]').first; + } else { + title = $('.copy > .title').first; + year = (type == 'movie')? + $('.copy > .details').first: + $('.summary ~ .title ~ *').first; + } + + if(!title) + return 1000; + + year = +year.textContent.slice(0, 4).trim(); + title = title.textContent.replace(RegExp(`\\s*\\(${ year }\\).*`), '').trim(); + image = (image || {}).src; + + return { type, title, year, image }; + }, + + "getType": () => { + let { pathname } = top.location; + + return /\bmovies?\b/i.test(pathname)? + 'movie': + /\bseries\b/i.test(pathname)? + 'show': + 'error' + }, + + ondemand: /\bondemand\b/i.test(top.location.pathname), + + watch: /\bwatch\b/i.test(top.location.pathname), +}; diff --git a/src/cloud/vrv.js b/src/cloud/vrv.js new file mode 100644 index 0000000..1e1f7af --- /dev/null +++ b/src/cloud/vrv.js @@ -0,0 +1,80 @@ +let script = { + "url": "*://*.vrv.co/(series|watch)/", + + "ready": () => { + let img = $('.h-thumbnail > img').first, + pre = $('#content .content .card').first; + + return script.getType('list')? pre && pre.textContent: img && img.src; + }, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let type = script.getType(), + title, year, image, options; + + switch(type) { + case 'movie': + case 'show': + title = $('.series, .series-title, .video-title, [class*="series"] .title, [class*="video"] .title').first; + year = $('.additional-information-item').first; + image = $('.series-poster img').first; + + title = title.textContent.replace(/(unrated|mature|tv-?\d{1,2})\s*$/i, '').trim(); + year = year? +year.textContent.replace(/.+(\d{4}).*/, '$1').trim(): 0; + image = (image || {}).src; + + options = { type, title, year, image }; + break; + + case 'list': + let items = $('#content .content .card'); + + options = []; + + items.forEach(element => { + let option = script.process(element); + + if(option) + options.push(option); + }); + break; + + default: + return 5000; + } + + return options; + }, + + "getType": (expected) => { + let type = 'error', + { pathname } = top.location; + + type = (/^\/(?:series)\//.test(pathname) || (/^\/(?:watch)\//.test(pathname) && !$('.content .series').empty))? + 'show': + (/^\/(?:watch)\//.test(pathname) && $('.content .series').empty)? + 'movie': + (/\/(watchlist)\b/i.test(pathname))? + 'list': + type; + + if(expected) + return type == expected; + + return type; + }, + + "process": (element) => { + let title = $('.info > *', element).first, + image = $('.poster-image img', element).first, + type = $('.info [class*="series"], .info [class*="movie"]', element).first; + + title = title.textContent.trim(); + image = image.src; + type = type.getAttribute('class').replace(/[^]*(movie|series)[^]*/, '$1'); + + return { type, title, image }; + }, +}; diff --git a/src/cloud/vudu.js b/src/cloud/vudu.js new file mode 100644 index 0000000..15af40a --- /dev/null +++ b/src/cloud/vudu.js @@ -0,0 +1,32 @@ +let script = { + "url": "*://*.vudu.com/*", + + "ready": () => !$('img[src*="poster" i]').empty, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('.head-big').first, + year = $('.container .row:first-child .row ~ * > .row span').first, + image = $('img[src*="poster" i]').first, + type = script.getType(); + + title = title.textContent.replace(/\((\d{4})\)/, '').trim(); + year = year? year.textContent.split(/\s*\|\s*/): R.$1; + image = (image || {}).src; + + if(!title) + return 5000; + + year = +year[year.length - 1].slice(0, 4); + year |= 0; + + return { type, title, year, image }; + }, + + "getType": () => { + return /(?:Season-\d+\/\d+)$/i.test(window.location.pathname)? + 'show': + 'movie'; + }, +}; diff --git a/src/cloud/vumoo.js b/src/cloud/vumoo.js new file mode 100644 index 0000000..1a32142 --- /dev/null +++ b/src/cloud/vumoo.js @@ -0,0 +1,68 @@ +let script = { + "url": "*://*.vumoo.to/(movies|tv-series)/*", + + "ready": () => !$('[role="presentation"i]').empty, + + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('.film-box h1').first, + year = $('.film-box > * span').filter(e => /\b\d{4}\b/.test(e.textContent))[0], + image = $('.poster').first, + type = script.getType(); + + title = title.textContent.replace(/\s*season\s+\d+\s*$/i, '').replace(/\s*\((\d{4})\)/, '').trim(); + year = +(type == 'movie')? + R.$1: + year.textContent.replace(/[^]*(\d{4})[^]*/, '$1'); + image = (image? image.src: null); + + // auto-prompt downloading for the user + let servers = $('.play'), + roles = $('[role="presentation"i] a'); + + if(servers.length > 1 && type != 'show') { + OLOAD_EVENTS.push(setTimeout( + () => Notify('update', 'Finding download links...', 3000), + 500 + )); + + servers.forEach((server, index, array) => OLOAD_EVENTS.push(setTimeout( + () => { + roles[index].click(); + server.click(); + + if(index == servers.length -1) + OLOAD_EVENTS.push(setTimeout( + () => Notify('update', 'No download links found'), + 7000 + )); + }, + index * 4500 + ))); + } + + return { type, title, year, image }; + }, + + "getType": () => { + let { pathname } = top.location; + + return pathname.startsWith('/movies')? + 'movie': + 'show'; + }, +}, + OLOAD_EVENTS = []; + +top.addEventListener('message', request => { + try { + request = request.data; + + if(request) + if(request.from == 'oload' || request.found == true) + OLOAD_EVENTS.forEach(timeout => clearTimeout(timeout)); + } catch(error) { + throw error; + } +}); diff --git a/src/cloud/webtoplex.js b/src/cloud/webtoplex.js new file mode 100644 index 0000000..f2f3bd9 --- /dev/null +++ b/src/cloud/webtoplex.js @@ -0,0 +1,57 @@ +// optional +// "Web to Plex" requires: api, token +// 'Friendly Name' requires permissions... + +let script = { + // required + "url": "*://ephellon.github.io/web.to.plex/(?!test|login)", + // Example: *://*.amazon.com/*/video/(detail|buy)/* + // *:// - match any protocol (http, https, etc.) + // *.amazon.com - match any sub-domain (www, ww5, etc.) + // /* - match any path + // (detail|buy) - match one of the items + + // optional + "ready": () => location.search && location.search.length > 1 && $('#tmdb').first.textContent, + + // optional + "timeout": 5000, // if the script fails to complete, retry after ... milliseconds + + // required + "init": (ready) => { + let _title, _year, _image, R = RegExp; + + let title = $('#title').first, + year = $('#year').first, + image = $('#poster').first, + type = script.getType(), // described below + IMDbID = script.getID('imdb')||"", + TMDbID = script.getID('tmdb')|0; + + title = title.textContent; + year = year.textContent|0; + image = image.src; + + return { type, title, year, image, IMDbID, TMDbID }; + }, + + // optional | functioanlity only + "getType": () => ($('#info').first.getAttribute('type') == 'movie'? 'movie': 'show'), + + "getID": (provider) => $(`#${provider}`).first.textContent, +}; + +(() => { + let login = /\blogin\b/.test(location.pathname), + apikey = $('#apikey').first; + + if(login && configuration.TMDbAPI && !apikey.value) { + apikey.value = configuration.TMDbAPI; + + return -1; + // no longer needed to run + } else if(login) { + return -1; + // don't run on the login page + } +})(); diff --git a/src/cloud/youtube.js b/src/cloud/youtube.js new file mode 100644 index 0000000..bf7a26b --- /dev/null +++ b/src/cloud/youtube.js @@ -0,0 +1,139 @@ +let openedByUser = false, + listenersSet = false, + listenerInt; + +let script = { + "url": "*://www.youtube.com/.+", + + "timeout": 1000, + + "init": (ready, rerun = false) => { + let _title, _year, _image, R = RegExp; + + let options, type, + alternative = $('#offer-module-container[class*="movie-offer"], #offer-module-container[class*="unlimited-offer"]'); + + if($('.more-button:not(span), .less-button').empty || !$('.opened').empty || !$('iron-dropdown[class*="ytd"][aria-hidden]').empty) + return script.timeout; + + // open and close the meta-information + // open + $('.more-button:not(span)').first.click(); + // close + setTimeout(() => $('.less-button').first.click(), script.timeout); + + // try to not bug the page content too much, use an alternative method first (if applicable) + if(!alternative.empty && !rerun) { + alternative = alternative.first; + + let title = $('#title', alternative).first, + year = $('#info p', alternative).child(2).lastElementChild, + image = $('#img img', alternative).first, + type = /\bmovie-offer\b/i.test(alternative.classList)? 'movie': 'show'; + + if(!title || !year) + return -1; + + title = title.textContent; + year = year.textContent|0; + image = image.src; + + title = title.replace(R(`\\s*(\\(\\s*)?${ year }\\s*(\\))?`), ''); + + return { type, title, year, image }; + } + + type = script.getType(); + + if(type == 'error') + return -1; + + if(type == 'movie' || type == 'show') { + let title = $((type == 'movie'? '.title': '#owner-container, #header #main-title')).first, + year = $('#content ytd-expander').first, + image = $('#img img').first || { src: '' }; + + if(!title) + return -1; + + title = title.textContent.trim(); + year = (year)? + +year.textContent.replace(/[^]*(?:release|air) date\s+(?:(?:\d+\/\d+\/)?(\d{2,4}))[^]*/i, ($0, $1, $$, $_) => +$1 < 1000? 2000 + +$1: $1): + YEAR; + image = (image)? + image.src: + null; + + title = title.replace(R(`\\s*(\\(\\s*)?${ year }\\s*(\\))?`), ''); + + options = { type, title, year }; + } else if(type == 'list') { + let title = $('#title').first, + year = $('#stats *').child(2), + image = $('#thumbnail #img').first; + + if(!title) + return -1; + + title = title.textContent.trim(); + year = parseInt(year.textContent); + image = (image || {}).src; + type = 'show'; + + options = { type, title, year, image }; + } else { + return -1; + } + + if(!listenersSet) { + listenerInt = setInterval(() => { + let closed = 'collapsed' in $('ytd-expander').first.attributes; + + if(closed && !openedByUser) + script.init(true); + }, 10); + + $('ytd-expander').first.addEventListener('mouseup', event => { + let closed = 'collapsed' in $('ytd-expander').first.attributes; + + if(!closed) + openedByUser = true; + else + openedByUser = false; + }); + + listenersSet = true; + } else { + clearInterval(listenerInt); + } + + return options; + }, + + "getType": () => { + let title = $('.super-title, #title, #header #main-title').filter(e => e.textContent)[0], + subtitle = $('#header #main-title + #sub-title').filter(e => e.textContent)[0], + owner = $('#owner-container, #upload-info [href^="/channel/"]'); + + if(owner.empty) + return 'error'; + else + owner = owner.first.textContent.replace(/^\s+|\s+$/g, ''); + + let R = { + movie: /\byoutube movies\b/i, + show : /\b(s\d+\b.+\be\d+|season \d+)\b/i, + list : /\/playlist\b/, + }; + + return (R.movie.test(owner))? + 'movie': + ((title && R.show.test(title.textContent)) || (subtitle && R.show.test(subtitle.textContent)))? + 'show': + (title && R.list.test(top.location.pathname))? + 'list': + 'error'; + }, +}; + +// $('a[href*="/watch?v="]').forEach(element => element.onclick = event => open(event.target.href, '_self')); diff --git a/src/download/consistent.js b/src/download/consistent.js new file mode 100644 index 0000000..d688b5b --- /dev/null +++ b/src/download/consistent.js @@ -0,0 +1,29 @@ +let NO_DEBUGGER = false; + +let terminal = + NO_DEBUGGER? + { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m }: + console; + +let check; + +check = document.body.onload = event => { + let video = document.querySelector('video'); + + if(video && (video.src || video.textContent)) { + let { src } = video; + + src = src || video.textContent; + + if(/^blob:/i.test(src)) + throw ' URL detected. Unable to reform file.'; + + try { + top.postMessage({ href: src, tail: 'MP4', type: 'SEND_VIDEO_LINK', from: 'consistent' }, '*'); + } catch(error) { + terminal.error('Failed to post message:', error); + } + } else { + setTimeout(check, 500); + } +}; diff --git a/src/download/oload.js b/src/download/oload.js index 2f28926..732f588 100644 --- a/src/download/oload.js +++ b/src/download/oload.js @@ -1,18 +1,29 @@ let NO_DEBUGGER = false; let terminal = - NO_DEBUGGER? - { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m }: - console; + NO_DEBUGGER? + { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m }: + console; -document.body.onload = event => { - let video = document.querySelector('div > p + p'); +let check; - if(video) { - try { - top.postMessage({ href: `https://oload.fun/stream/${ video.textContent }?mime=true`, tail: 'mp4', type: 'SEND_VIDEO_LINK', from: 'oload' }, '*'); - } catch(error) { - terminal.log('Failed to post message:', error); - } - } +check = document.body.onload = event => { + let video = document.querySelector('div > p + p'); + + if(video && (video.src || video.textContent)) { + let { src } = video; + + src = src || video.textContent; + + if(/^blob:/i.test(src)) + throw ' URL detected. Unable to reform file.'; + + try { + top.postMessage({ href: `https://oload.fun/stream/${ src }?mime=true`, tail: 'MP4', type: 'SEND_VIDEO_LINK', from: 'oload' }, '*'); + } catch(error) { + terminal.error('Failed to post message:', error); + } + } else { + setTimeout(check, 500); + } }; diff --git a/src/download/plex.js b/src/download/plex.js new file mode 100644 index 0000000..86fab87 --- /dev/null +++ b/src/download/plex.js @@ -0,0 +1,146 @@ +/** plxdwnld - Pip Longrun / Ephellon +* +* This project is licensed under the terms of the MIT license, see https://piplongrun.github.io/plxdwnld/LICENSE.txt +* +* @author Pip Longrun +* @version 0.2 +* @see https://piplongrun.github.io/plxdwnld/ +* +*/ + +let plxdwnld = (() => { + let self = {}, R = RegExp, + baseURI, AccessToken, + RegExps = { + clientID: /server\/([a-f\d]{40})\//i, + metadataID: /key=%2Flibrary%2Fmetadata%2F(\d+)/i, + }, + URLExps = { + API_resource: 'https://plex.tv/api/resources?includeHttps=1&X-Plex-Token={token}', + API_library: '{baseuri}/library/metadata/{id}?X-Plex-Token={token}', + download: '{baseuri}{partkey}?download=1&X-Plex-Token={token}', + }, + access_token_path = '//Device[@clientIdentifier=\'{clientID}\']/@accessToken', + base_uri_path = '//Device[@clientIdentifier=\'{clientID}\']/Connection[@local=0]/@uri', + part_key_path = '//Media/Part[1]/@key'; + + // Errors + let ERROR = { + EMPTY: 'No response data was received', + NOT_PLEX: 'You are not browsing (or logged into) Plex', + NOT_MEDIA: 'You are not viewing a media item', + INVALID_TOKEN: 'Unable to find a valid Access Token', + }; + + let getXML = (url, callback) => { + fetch(`//cors-anywhere.herokuapp.com/${ url }`, { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest' } }) + .then(Q => Q.text()) + .then(text => { + if(!text.length) + throw ERROR.EMPTY; + + let Parser = new DOMParser(), + XML = Parser.parseFromString(text, 'text/xml'); + + callback(XML); + }) + .catch(error => { throw error }); + }; + + let getMetadata = (XML) => { + let clientID = RegExps.clientID.test(location.href)? + R.$1: + null; + + if(clientID) { + let access_token_node = XML.evaluate( + access_token_path.replace(/{clientid}/ig, clientID), + XML, + null, + XPathResult.FIRST_ORDERED_NODE_TYPE, + null + ), + base_uri_node = XML.evaluate( + base_uri_path.replace(/{clientid}/ig, clientID), + XML, + null, + XPathResult.FIRST_ORDERED_NODE_TYPE, + null + ); + + if(access_token_node.singleNodeValue && base_uri_node.singleNodeValue) { + AccessToken = access_token_node.singleNodeValue.textContent; + baseURI = base_uri_node.singleNodeValue.textContent; + + let metadataID = RegExps.metadataID.test(location.href)? + R.$1: + null; + + if(metadataID) + getXML( + URLExps.API_library + .replace(/{baseuri}/ig, baseURI) + .replace(/{id}/ig, metadataID) + .replace(/{token}/ig, AccessToken) + , GetDownloadURL + ); + else + throw ERROR.NOT_MEDIA; + } else { + throw ERROR.INVALID_TOKEN; + } + } else { + throw ERROR.NOT_MEDIA; + } + }; + + let GetDownloadURL = (XML) => { + let part_key_node = XML.evaluate(part_key_path, XML, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); + + if(part_key_node.singleNodeValue) { + let href = URLExps.download + .replace(/{baseuri}/ig, baseURI) + .replace(/{partkey}/ig, part_key_node.singleNodeValue.textContent) + .replace(/{token}/ig, AccessToken); + + top.postMessage({ href, tail: 'MP4', type: 'SEND_VIDEO_LINK', from: 'plex' }, '*'); + } else { + throw ERROR.NOT_MEDIA; + } + }; + + self.init = () => { + if(localStorage.myPlexAccessToken !== undefined) + getXML(URLExps.API_resource.replace(/{token}/ig, localStorage.myPlexAccessToken), getMetadata); + else + throw ERROR.NOT_PLEX; + }; + + return self; +})(); + +let NO_DEBUGGER = false; + +let terminal = + NO_DEBUGGER? + { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m }: + console; + +let check; + +check = document.body.onload = event => { + let loading = document.querySelector('.loading'); + + if(!loading) { + setTimeout(() => { + try { + plxdwnld.init(); + } catch(error) { + terminal.error('Failed to post message:', error); + setTimeout(check, 5000); + } + }, 5000) + } else { + setTimeout(check, 500); + } +}; diff --git a/src/helpers.js b/src/helpers.js index 96caee1..0c5c27e 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -1,204 +1,57 @@ -(() => { - -String.prototype.toCaps = function toCaps(all) { - /** Titling Caplitalization - * Articles: a, an, & the - * Conjunctions: and, but, for, nor, or, so, & yet - * Prepositions: across, after, although, at, because, before, between, by, during, from, if, in, into, of, on, to, through, under, with, & without - */ - let array = this.toLowerCase(), - titles = /(?!^|(?:an?|the)\s+)\b(a([st]|nd?|cross|fter|lthough)?|b(e(cause|fore|tween)|ut|y)|during|from|in(to)?|[io][fn]|[fn]?or|the|[st]o|through|under|with(out)?|yet)(?!\s*$)\b/gi, - cap_exceptions = /([\|\"\(]\s*[a-z]|[\:\.\!\?]\s+[a-z]|(?:^\b|[^\'\-\+]\b)[^aeiouy\d\W]+\b)/gi, // Punctuation exceptions, e.g. "And not I" - all_exceptions = /\b((?:ww)?(?:m+[dclxvi]*|d+[clxvi]*|c+[lxvi]*|l+[xvi]*|x+[vi]*|v+i*|i+))\b/gi, // Roman Numberals - cam_exceptions = /\b((?:mr?s|[sdjm]r|mx)|(?:adm|cm?dr?|chf|c[op][lmr]|cpt|gen|lt|mjr|sgt)|doc|hon|prof)\./gi; // Titles (Most Common?) - - array = array.split(/\s+/); - - let index, length, string, word; - for(index = 0, length = array.length, string = [], word; index < length; index++) { - word = array[index]; - - if(word) - string.push( word[0].toUpperCase() + word.slice(1, word.length) ); - } - - string = string.join(' '); - - if(!all) - string = string - .replace(titles, ($0, $1, $$, $_) => $1.toLowerCase()) - .replace(cap_exceptions, ($0, $1, $$, $_) => $1.toUpperCase()) - .replace(all_exceptions, ($0, $1, $$, $_) => $1.toUpperCase()) - .replace(cam_exceptions, ($0, $1, $$, $_) => $0[0].toUpperCase() + $0.slice(1, $0.length).toLowerCase()); - - return string; -}; - -(function(parent) { -/* SortBy.js */ -/** Usage + Example - // document.queryBy( selectors )... - - let index = 0; - // the order given is the order handled - document.queryBy("div:last-child, div:nth-child(2), div:first-child") - .forEach((element, index, array) => element.innerHTML = index + 1); - - // output w/sortBySelector: -
3
-
2
-
1
- - // output w/o sortBySelector: -
1
-
2
-
3
- */ - parent.queryBy = function queryBy(selectors, container = parent) { - // Helpers - let copy = array => [].slice.call(array), - query = (SELECTORS, CONTAINER = container) => CONTAINER.querySelectorAll(SELECTORS); - - // Get rid of enclosing syntaxes: [...] and (...) - let regexp = /(\([^\(\)]+?\)|\[[^\[\]]+?\])/g, - pulled = [], - media = [], - index, length; - - // The index shouldn't be longer than the length of the selector's string - // Keep this to prevent infinite loops - for(index = 0, length = selectors.length; index++ < length && regexp.test(selectors);) - selectors = selectors.replace(regexp, ($0, $1, $$, $_) => '\b--' + pulled.push($1) + '\b'); - - let order = selectors.split(','), - dummy = copy(order), - output = [], - generations = 0; - - // Replace those syntaxes (they were ignored) - for(index = 0, length = dummy.length, order = [], regexp = /[\b]--(\d+)[\b]/g; index < length; index++) - order.push(dummy[index].replace(regexp, ($0, $1, $$, $_) => pulled[+$1 - 1])); - - // Make sure to put the elements in order - // Handle the :parent (pseudo) selector - for(index = 0, length = order.length; index < length; generations = 0, index++) { - let selector = order[index], ancestor; - - selector = selector - .replace(/\:nth-parent\((\d+)\)/g, ($0, $1, $$, $_) => (generations -= +$1, '')) - .replace(/(\:{1,2}parent\b|<\s*(\*|\s*(,|$)))/g, ($0, $$, $_) => (--generations, '')) - .replace(/<([^<,]+)?/g, ($0, $1, $$, $_) => (ancestor = $1, --generations, '')); - - let elements = query(selector), - parents = [], parent; - - for(; generations < 0; generations++) - elements.forEach( element => { - let P = element, - E = C => [].slice.call(query(ancestor, C)), - F; - - for(let I = 0, L = -generations; ancestor && !!P && I < L; I++) - P = !!~E(P.parentElement).indexOf(P)? P: P.parentElement; - - parent = ancestor? !~E(P.parentElement).indexOf(P)? null: P: P.parentElement; - - if(!~parents.indexOf(parent)) - parents.push(parent); - }); - media.push(parents.length? parents: elements); - } - - // Create a continuous array from the sub-arrays - for(index = 1, length = media.length; index < length; index++) - media.splice(0, 1, copy(media[0]).concat( copy(media[index]) )); - output = [].slice.call(media[0]).filter( value => value ); - - // Remove repeats - for(index = 0, length = output.length, media = []; index < length; index++) - if(!~media.indexOf(output[index])) - media.push(output[index]); - - let properties = { writable: false, enumerable: false, configurable: false }; - - Object.defineProperties(media, { - first: { - value: media[0], - ...properties - }, - last: { - value: media[media.length - 1], - ...properties - }, - child: { - value: index => media[index - 1], - ...properties - } - }); - - return media; - }; - -/** Adopted from - * LICENSE: MIT (2018) - */ - parent.furnish = function furnish(name, attributes = {}, ...children) { - let u = v => v && v.length, R = RegExp; - - if( !u(name) ) - throw TypeError(`TAGNAME cannot be ${ (name === '')? 'empty': name }`); - - let options = attributes.is === true? { is: true }: null; - - delete attributes.is; - - name = name.split(/([#\.][^#\.\[\]]+)/).filter( u ); - - if(name.length <= 1) - name = name[0].split(/^([^\[\]]+)(\[.+\])/).filter( u ); - - if(name.length > 1) - for(let n = name, i = 1, l = n.length, t, v; i < l; i++) - if((v = n[i].slice(1, n[i].length)) && (t = n[i][0]) == '#') - attributes.id = v; - else if(t == '.') - attributes.classList = [].slice.call(attributes.classList || []).concat(v); - else if(/\[(.+)\]/.test(n[i])) - R.$1.split('][').forEach(N => attributes[(N = N.split('=', 2))[0]] = N[1] || ''); - name = name[0]; - - let element = document.createElement(name, options); - - if(attributes.classList instanceof Array) - attributes.classList = attributes.classList.join(' '); - - Object.entries(attributes).forEach( - ([name, value]) => (/^(on|(?:inner|outer)(?:HTML|Text)|textContent|class(?:List|Name)$|value)/.test(name))? - element[name] = value: - element.setAttribute(name, value) - ); - - children - .filter( child => child !== undefined && child !== null ) - .forEach( - child => - child instanceof Element? - element.append(child): - child instanceof Node? - element.appendChild(child): - element.appendChild( - parent.createTextNode(child) - ) - ); - - return element; - } -})(document); - -let PRIMITIVE = Symbol.toPrimitive, - queryBy = document.queryBy, - furnish = document.furnish; - -queryBy[PRIMITIVE] = furnish[PRIMITIVE] = String.prototype.toCaps[PRIMITIVE] = () => 'function () { [foreign code] }'; - -})(); +async function load(name = '') { + if(!name) return; + + let HELPERS_STORAGE = chrome.storage.sync || chrome.storage.local; + + name = '~/cache/' + (name.toLowerCase().replace(/\s+/g, '_')); + + return new Promise((resolve, reject) => { + function LOAD(DISK) { + let data = JSON.parse(DISK[name] || null); + + return resolve(data); + } + + HELPERS_STORAGE.get(null, DISK => { + if (chrome.runtime.lastError) + chrome.storage.local.get(null, LOAD); + else + LOAD(DISK); + }); + }); +} + +async function save(name = '', data) { + if(!name) return; + + let HELPERS_STORAGE = chrome.storage.sync || chrome.storage.local; + + name = '~/cache/' + (name.toLowerCase().replace(/\s+/g, '_')); + data = JSON.stringify(data); + + await HELPERS_STORAGE.set({[name]: data}, () => data); + + return name; +} + +async function kill(name) { + let HELPERS_STORAGE = chrome.storage.sync || chrome.storage.local; + + return HELPERS_STORAGE.remove(['~/cache/' + (name.toLowerCase().replace(/\s+/g, '_'))]); +} + +async function Notify(state, text, timeout = 7000, requiresClick = true) { + return top.postMessage({ type: 'NOTIFICATION', data: { state, text, timeout, requiresClick } }, '*'); +} + +async function Require(permission, name, alias) { + let allowed = await load(`has/${ name }`), + allotted = await load(`get/${ name }`); + + top.postMessage({ type: 'PERMISSION', data: { permission, name, alias, allowed, allotted } }); + + /* Already asked for permission */ + if(typeof allowed == 'boolean') + /* The allowed permission(s) */ + return allotted; +} diff --git a/src/history-hack.js b/src/history-hack.js index 8a6db46..4fb99a3 100644 --- a/src/history-hack.js +++ b/src/history-hack.js @@ -4,21 +4,21 @@ let __script__ = document.createElement('script'); // It can modify objects and functions of the page __script__.text = `(${ function() { - let history = window.history, - __pushState__ = history.pushState, - __replaceState__ = history.replaceState; + let history = window.history, + __pushState__ = history.pushState, + __replaceState__ = history.replaceState; - history.pushState = function(state, title, url) { - __pushState__.call(this, state, title, url); + history.pushState = function(state, title, url) { + __pushState__.call(this, state, title, url); - window.dispatchEvent(new CustomEvent('pushstate-changed', { detail: state })); - }; + window.dispatchEvent(new CustomEvent('pushstate-changed', { detail: state })); + }; - history.replaceState = function(state, title, url) { - __replaceState__.call(this, state, title, url); + history.replaceState = function(state, title, url) { + __replaceState__.call(this, state, title, url); - window.dispatchEvent(new CustomEvent('pushstate-changed', { detail: state })); - }; + window.dispatchEvent(new CustomEvent('pushstate-changed', { detail: state })); + }; } })();`; diff --git a/src/img/$$$32.png b/src/img/$$$32.png new file mode 100644 index 0000000..1a38cb2 Binary files /dev/null and b/src/img/$$$32.png differ diff --git a/src/img/$$32.png b/src/img/$$32.png new file mode 100644 index 0000000..6bb8609 Binary files /dev/null and b/src/img/$$32.png differ diff --git a/src/img/$32.png b/src/img/$32.png new file mode 100644 index 0000000..e192dbf Binary files /dev/null and b/src/img/$32.png differ diff --git a/src/img/32.png b/src/img/32.png new file mode 100644 index 0000000..e070143 Binary files /dev/null and b/src/img/32.png differ diff --git a/src/img/96.png b/src/img/96.png new file mode 100644 index 0000000..1c7b3c1 Binary files /dev/null and b/src/img/96.png differ diff --git a/src/img/_32.png b/src/img/_32.png new file mode 100644 index 0000000..d5c6c18 Binary files /dev/null and b/src/img/_32.png differ diff --git a/src/img/allocine.png b/src/img/allocine.png new file mode 100644 index 0000000..c5375d6 Binary files /dev/null and b/src/img/allocine.png differ diff --git a/src/img/justwatch.png b/src/img/justwatch.png new file mode 100644 index 0000000..7f4bf1f Binary files /dev/null and b/src/img/justwatch.png differ diff --git a/src/img/local.medusa.png b/src/img/local.medusa.png new file mode 100644 index 0000000..36e775d Binary files /dev/null and b/src/img/local.medusa.png differ diff --git a/src/img/local.sickBeard.png b/src/img/local.sickBeard.png new file mode 100644 index 0000000..bc16cb3 Binary files /dev/null and b/src/img/local.sickBeard.png differ diff --git a/src/img/moviemeter.png b/src/img/moviemeter.png new file mode 100644 index 0000000..b153f79 Binary files /dev/null and b/src/img/moviemeter.png differ diff --git a/src/img/tubi.png b/src/img/tubi.png new file mode 100644 index 0000000..5a8ce56 Binary files /dev/null and b/src/img/tubi.png differ diff --git a/src/img/vumoo.png b/src/img/vumoo.png new file mode 100644 index 0000000..ddf273a Binary files /dev/null and b/src/img/vumoo.png differ diff --git a/src/manifest.json b/src/manifest.json index 41273b8..0b7971c 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,128 +1,197 @@ { - "update_url": "https://github.com/Ephellon/web-to-plex/raw/master/src.crx", +// "update_url": "https://ephellon.github.com/web.to.plex/update.xml", "name": "Web to Plex", "description": "Adds a button on various movie & TV show sites to open the item in Plex, or send to your designated NZB manager for download.", - "homepage_url": "https://github.com/Ephellon/web-to-plex/", + "homepage_url": "https://github.com/SpaceK33z/web-to-plex/", "manifest_version": 2, - "version": "4.1", + "version": "4.1.0.1", // Firefox Support => -// "applications": { -// "gecko": { -// "id": "minkcbos@gmail.com", -// "strict_min_version": "57.0" -// } -// }, +// "applications": { +// "gecko": { +// "id": "{05243336-ce19-46df-95af-680070c96134}", +// "strict_min_version": "57.0" +// } +// }, "icons": { - "16": "img/16.png", - "48": "img/48.png", + "16": "img/16.png", + "32": "img/32.png", + "48": "img/48.png", + "96": "img/96.png", "128": "img/128.png", - "256": "img/256.png" + "256": "img/256.png" }, "content_scripts": [ - // Allows media downloads - { - "matches": ["*://*.openload.co/*", "*://*.openload.com/*", "*://*.oload.fun/*"], - "js": ["download/oload.js"], - "all_frames": true - }, + // Allows media downloads + { + "matches": [ + "*://*.openload.co/*", "*://*.oload.co/*", + "*://*.openload.com/*", "*://*.oload.com/*", + "*://*.openload.fun/*", "*://*.oload.fun/*", + "*://*.openload.biz/*", "*://*.oload.biz/*", + "*://*.openload.vip/*", "*://*.oload.vip/*", + "*://*.openload.club/*", "*://*.oload.club/*", + "*://*.openload.io/*", "*://*.oload.io/*", + "*://*.openload.xyz/*", "*://*.oload.xyz/*", + "*://*.openload.cc/*", "*://*.oload.cc/*", + "*://*.openload.to/*", "*://*.oload.to/*", + "*://*.openload.is/*", "*://*.oload.is/*", + "*://*.openload.gg/*", "*://*.oload.gg/*", + "*://*.openload.tv/*", "*://*.oload.tv/*", + "*://*.openload.fm/*", "*://*.oload.fm/*", + "*://*.openload.cx/*", "*://*.oload.cx/*", + "*://*.openload.ac/*", "*://*.oload.ac/*", + "*://*.openload.name/*", "*://*.oload.name/*", + "*://*.openload.global/*", "*://*.oload.global/*" + ], + "js": ["download/oload.js"], + "all_frames": true + }, + { + "matches": ["*://*.consistent.stream/titles/*", "*://*.consistent.stream/watch/*"], + "js": ["download/consistent.js"], + "all_frames": true + }, + { + "matches": ["*://app.plex.tv/desktop#!/server/*/details?*"], + "js": ["download/plex.js"], + "all_frames": true + }, - // The sites - { + // Testing purposes only + { + "matches": ["*://ephellon.github.io/web.to.plex/test/*"], + "js": ["utils.js", "sites/__test__.js"], + "css": ["sites/common.css", "sites/theme.css"] + }, + + // The sites + { "matches": ["*://*.movieo.me/*"], "js": ["history-hack.js", "utils.js", "sites/movieo/index.js"], - "css": ["sites/movieo/index.css", "sites/common.css"] + "css": ["sites/movieo/index.css", "sites/common.css", "sites/theme.css"] },{ "matches": ["*://*.imdb.com/*"], "js": ["utils.js", "sites/imdb/index.js"], - "css": ["sites/imdb/index.css", "sites/common.css"] + "css": ["sites/imdb/index.css", "sites/common.css", "sites/theme.css"] },{ "matches": ["*://*.trakt.tv/*"], "js": ["history-hack.js", "utils.js", "sites/trakt/index.js"], - "css": ["sites/trakt/index.css", "sites/common.css"] + "css": ["sites/trakt/index.css", "sites/common.css", "sites/theme.css"] },{ "matches": ["*://*.letterboxd.com/*"], "js": ["utils.js", "sites/letterboxd/index.js"], - "css": ["sites/letterboxd/index.css", "sites/common.css"] + "css": ["sites/letterboxd/index.css", "sites/common.css", "sites/theme.css"] },{ "matches": ["*://*.tvmaze.com/shows/*"], "js": ["utils.js", "sites/tvmaze/index.js"], - "css": ["sites/tvmaze/index.css", "sites/common.css"] + "css": ["sites/tvmaze/index.css", "sites/common.css", "sites/theme.css"] },{ "matches": ["*://*.thetvdb.com/series/*"], "js": ["utils.js", "sites/tvdb/index.js"], - "css": ["sites/tvdb/index.css", "sites/common.css"] + "css": ["sites/tvdb/index.css", "sites/common.css", "sites/theme.css"] },{ - "matches": ["*://*.themoviedb.org/movie/*", "https://*.themoviedb.org/tv/*"], + "matches": ["*://*.themoviedb.org/movie/*", "*://*.themoviedb.org/tv/*"], "js": ["utils.js", "sites/tmdb/index.js"], - "css": ["sites/tmdb/index.css", "sites/common.css"] + "css": ["sites/tmdb/index.css", "sites/common.css", "sites/theme.css"] },{ "matches": ["*://*.vrv.co/*"], "js": ["utils.js", "sites/vrv/index.js"], - "css": ["sites/vrv/index.css", "sites/common.css"] + "css": ["sites/vrv/index.css", "sites/common.css", "sites/theme.css"] },{ "matches": ["*://*.hulu.com/*"], "js": ["utils.js", "sites/hulu/index.js"], - "css": ["sites/hulu/index.css", "sites/common.css"] + "css": ["sites/hulu/index.css", "sites/common.css", "sites/theme.css"] },{ "matches": ["*://play.google.com/store/*"], "js": ["utils.js", "sites/google/play.js"], - "css": ["sites/google/index.css", "sites/common.css"] + "css": ["sites/google/index.css", "sites/common.css", "sites/theme.css"] },{ "matches": ["*://itunes.apple.com/*"], "js": ["utils.js", "sites/itunes/index.js"], - "css": ["sites/itunes/index.css", "sites/common.css"] + "css": ["sites/itunes/index.css", "sites/common.css", "sites/theme.css"] },{ "matches": ["*://*.metacritic.com/*"], "js": ["utils.js", "sites/metacritic/index.js"], - "css": ["sites/metacritic/index.css", "sites/common.css"] + "css": ["sites/metacritic/index.css", "sites/common.css", "sites/theme.css"] },{ "matches": ["*://*.fandango.com/*"], "js": ["utils.js", "sites/fandango/index.js"], - "css": ["sites/fandango/index.css", "sites/common.css"] + "css": ["sites/fandango/index.css", "sites/common.css", "sites/theme.css"] },{ "matches": ["*://*.amazon.com/*"], "js": ["utils.js", "sites/amazon/index.js"], - "css": ["sites/amazon/index.css", "sites/common.css"] + "css": ["sites/amazon/index.css", "sites/common.css", "sites/theme.css"] },{ "matches": ["*://*.vudu.com/*"], "js": ["utils.js", "sites/vudu/index.js"], - "css": ["sites/vudu/index.css", "sites/common.css"] + "css": ["sites/vudu/index.css", "sites/common.css", "sites/theme.css"] },{ "matches": ["*://*.verizon.com/*"], "js": ["utils.js", "sites/verizon/index.js"], - "css": ["sites/verizon/index.css", "sites/common.css"] + "css": ["sites/verizon/index.css", "sites/common.css", "sites/theme.css"] },{ "matches": ["*://*.couchpotato.life/*/*"], "js": ["utils.js", "sites/couchpotato/index.js"], - "css": ["sites/couchpotato/index.css", "sites/common.css"] + "css": ["sites/couchpotato/index.css", "sites/common.css", "sites/theme.css"] },{ "matches": ["*://*.rottentomatoes.com/*/*"], "js": ["utils.js", "sites/rottentomatoes/index.js"], - "css": ["sites/rottentomatoes/index.css", "sites/common.css"] + "css": ["sites/rottentomatoes/index.css", "sites/common.css", "sites/theme.css"] },{ "matches": ["*://*.netflix.com/watch/*"], "js": ["utils.js", "sites/netflix/index.js"], - "css": ["sites/netflix/index.css", "sites/common.css"] + "css": ["sites/netflix/index.css", "sites/common.css", "sites/theme.css"] + },{ + "matches": ["*://*.vumoo.to/*"], + "js": ["utils.js", "sites/vumoo/index.js"], + "css": ["sites/vumoo/index.css", "sites/common.css", "sites/theme.css"] },{ - "matches": ["*://*.gostream.site/*"], - "js": ["utils.js", "sites/gostream/index.js"], - "css": ["sites/gostream/index.css", "sites/common.css"] - },{ "matches": ["*://www.google.com/*"], "js": ["utils.js", "sites/google/index.js"], - "css": ["sites/google/index.css", "sites/common.css"] + "css": ["sites/google/index.css", "sites/common.css", "sites/theme.css"] },{ "matches": ["*://www.youtube.com/*"], "js": ["utils.js", "sites/youtube/index.js"], - "css": ["sites/youtube/index.css", "sites/common.css"] + "css": ["sites/youtube/index.css", "sites/common.css", "sites/theme.css"] },{ "matches": ["*://*.flickmetrix.com/*"], "js": ["utils.js", "sites/flickmetrix/index.js"], - "css": ["sites/flickmetrix/index.css", "sites/common.css"] + "css": ["sites/flickmetrix/index.css", "sites/common.css", "sites/theme.css"] + },{ + "matches": ["*://*.justwatch.com/*"], + "js": ["utils.js", "sites/justwatch/index.js"], + "css": ["sites/justwatch/index.css", "sites/common.css", "sites/theme.css"] + },{ + "matches": ["*://*.moviemeter.nl/*"], + "js": ["utils.js", "sites/moviemeter/index.js"], + "css": ["sites/moviemeter/index.css", "sites/common.css", "sites/theme.css"] + },{ + "matches": ["*://*.allocine.fr/*"], + "js": ["utils.js", "sites/allocine/index.js"], + "css": ["sites/allocine/index.css", "sites/common.css", "sites/theme.css"] + },{ + "matches": ["*://*.gostream.site/*"], + "js": ["utils.js", "sites/gostream/index.js"], + "css": ["sites/gostream/index.css", "sites/common.css", "sites/theme.css"] + },{ + "matches": ["*://*.tubitv.com/*"], + "js": ["utils.js", "sites/tubi/index.js"], + "css": ["sites/tubi/index.css", "sites/common.css", "sites/theme.css"] + },{ + "matches": ["*://ephellon.github.io/web.to.plex/?*", "*://ephellon.github.io/web.to.plex/index.html?*", "*://ephellon.github.io/web.to.plex/login.html?*"], + "js": ["utils.js", "sites/webtoplex/index.js"], + "css": ["sites/webtoplex/index.css", "sites/common.css", "sites/theme.css"] + },{ + "matches": ["*://app.plex.tv/desktop/*"], + "js": ["utils.js", "sites/plex/index.js"], + "css": ["sites/plex/index.css", "sites/common.css", "sites/theme.css"] + },{ + "matches": ["https://*/*"], + "js": ["utils.js", "sites/common.js"] } ], @@ -131,27 +200,30 @@ "persistent": true }, + // Disable/Comment "options_page" to enable FF support "options_page": "options/index.html", - "options_ui": { - "page": "options/index.html", - "open_in_tab": true - }, + "options_ui": { + "page": "options/index.html", + "open_in_tab": true + }, - "browser_action": { - "default_icon": { - "16": "img/16.png", - "48": "img/48.png" - }, - "default_title": "Web to Plex", - "default_popup": "popup/index.html" - }, + "browser_action": { + "default_icon": { + "16": "img/16.png", + "32": "img/32.png", + "48": "img/48.png", + "96": "img/96.png" + }, + "default_title": "Web to Plex", + "default_popup": "popup/index.html" + }, "permissions": [ - "tabs", + "tabs", "storage", - "downloads", - "contextMenus", - "" + "downloads", + "contextMenus", + "" ], - "web_accessible_resources": ["img/*"] + "web_accessible_resources": ["img/*", "options/*"] } diff --git a/src/options/index.css b/src/options/index.css new file mode 100644 index 0000000..2083ac0 --- /dev/null +++ b/src/options/index.css @@ -0,0 +1,647 @@ +html, body { + height: 100%; +} + +body { + background: url(../img/noise.png) fixed, url(../img/256.png) no-repeat fixed center, #3f4245 !important; + color: #333 !important; + font-family: Open Sans Regular, Helvetica Neue, Helvetica, Arial, sans-serif, system; + font-size: 18px !important; + padding: 1em; + width: 90vw; + flex-grow: 1; + padding: 25px; + overflow: auto; + position: absolute; +} + +a { + color: #cc7b19 !important; + text-decoration: none !important; +} + +a[target="_blank"]::after { + content: " [\2197]"; + font-size: 70%; + + vertical-align: super; +} + +h1 { + position: sticky; + top: 0; +} + +h2 { + text-align: center; +} + +hr { + border: 1px solid #6668; +} +hr:last-child { + display: none; +} + +article { + z-index: 3; +} + +section { + display: block !important; + margin-bottom: 20px; + box-sizing: border-box; +} + +label { + color: #eee !important; + font-weight: 400 !important; + display: inline-block; + margin-bottom: 5px; +} + +input[type="text"], input[type="password"], textarea, select { + width: 30vw !important; + line-height: 1.5em !important; + transition: background 0.2s; + display: block !important; + height: 38px !important; + padding: 6px 12px; + font-size: 16px !important; + color: #eee !important; + vertical-align: middle; + background: rgba(255, 255, 255, 0.25); + border: 3px solid rgba(0, 0, 0, 0); + border-radius: 3px; + font-family: inherit; + margin: 0; +} + +textarea, select[multiple] { + height: 114px !important; +} + +div:not(body > div) { + color: rgba(255, 255, 255, 0.45) !important; + display: block; + margin-top: 5px !important; + margin-bottom: 10px !important; + box-sizing: border-box; + font-size: 14px !important; + z-index: 18 !important; +} + +button, input[type="button"i], .button { + padding: 10px 18px !important; + font-size: 16px !important; + line-height: 1.33 !important; + border-radius: 3px; + font-family: inherit; + text-transform: uppercase; + border: 0; + box-shadow: none !important; + position: relative; + overflow: hidden; + color: #fff !important; + background: #cc7b19 !important; + margin-bottom: 0; + font-weight: 400 !important; + vertical-align: middle; + cursor: pointer !important; + white-space: nowrap; + user-select: none; + transition: all 0.1s; +} + +button:hover, input[type="button"i]:hover, .button:hover { + background: #e59029 !important; +} + +[id$="_test"] { + background: #cc7b19 !important; + margin-bottom: 2px; + padding: 10px 8px 10px 10px !important; +} + +[id$="_status"] { + padding: 0 6px !important; + font-size: 16px !important; + border-radius: 3px; + font-family: monospace, sans-serif, sans, arial; + border: 0; + box-shadow: none !important; + color: #fff !important; + background: #666 !important; + border-radius: 4px; +} + +[id$="_status"].false { + background: #cc1b19 !important; +} + +[id$="_status"].true { + background: #7bcc19 !important; +} + +[id$="token"], [data-option$="Token"], [data-option$="API"] { + font-family: monospace, consolas, sans-serif, sans serif, sans, arial; +} + +[id$="-token"]:not(:placeholder-shown), [data-option$="-Token"]:not(:placeholder-shown), [data-option$="-API"]:not(:placeholder-shown) { + text-transform: uppercase; +} + +em { + color: #cc7b19 !important; +} + +select { + margin-left: 10px !important; + font-size: 16px !important; + line-height: inherit; + text-transform: none; +} + +#footer { + text-align: center; + + bottom: 0; + left: 0; + position: fixed; + + width: 100%; +} + +[in-use] { + color: #197bcc; +} + +[in-use]::after { + content: '\2610'; +} + +[in-use="true"i]::after { + content: '\2611'; +} + +[top] { + border-bottom-left-radius: 0 !important; + border-bottom-right-radius: 0 !important; +} + +[bottom] { + border-top-left-radius: 0 !important; + border-top-right-radius: 0 !important; +} + +/* notifications */ +.notification { + background: #F45A26; + border-radius: 4px; + color: #FFF !important; + cursor: pointer; + display: block; + font-family: arial, verdana, sans-serif; + font-size: 20px; + text-align: center; + + position: fixed; + left: 50%; + margin-left: -175px; + padding: 10px; + top: 80px; + + width: 350px; + z-index: 999999; +} + +/* Web to Plex general information notifications */ +.notification.info { + background: #666!important; +} + +/* Web to Plex update notifications */ +.notification.update { + background: #2A2AFF!important; +} + +/* Web to Plex warning notifications */ +.notification.warning { + background: #FF2A2A!important; +} + +/* Web to Plex prompt */ +.prompt { + background: #0008!important; + box-sizing: border-box!important; + color: #eee!important; + display: block!important; + font-family: Open Sans Regular, Helvetica Neue, Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 24px; + overflow: auto; + + height: 100%!important; + width: 100%!important; + + bottom: 0!important; + left: 0!important; + right: 0!important; + top: 0!important; + position: fixed!important; + z-index: 999999999!important; +} + +.prompt-body { + background: #282828; + box-shadow: 0 5px 15px #0008; + display: block; + + left: 20%; + top: 5%; + padding-top: 10px; + padding-bottom: 70px; + position: relative; + + height: 60%; + width: 60%; +} + +.prompt-header, .prompt-footer { + background: #323232; + border: 1px solid #0000; + box-sizing: border-box; + color: #eee; + text-size-adjust: 100%; + + margin-top: 0; + padding: 15px 20px; + position: absolute; + + height: 65px; + width: 100%; + + -webkit-tap-highlight-color: #0000; +} + +.prompt-header { + text-align: left; + border-bottom-color: #222; + border-bottom-width: 1px; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + + top: 0; +} + +.prompt-options { + display: block; + overflow-x: hidden; + overflow-y: auto; + + padding: 12px; + position: relative; + top: 65px; + + max-height: calc(100% - 65px); +} + +.prompt-option { + background: #323232; + border: 1px solid #202020; + border-radius: 3px; + color: #999; + display: block; + text-align: left; + + margin-bottom: 10px; + padding: 10px; + + min-height: 20px; +} + +.prompt-option.mutable { + max-width: 60%; +} + +.prompt-option.mutable > *:last-child { + background: #ffffff40; + border-radius: 3px; + transition: all 0.1s; + + height: 30px; + width: 30px; + + float: right; + margin-right: -9px; + margin-top: -9px; + padding: 0; +} + +.prompt-option.mutable > *:last-child:hover { + background: #ffffff4d; +} + +.prompt-option.mutable > *:last-child::after { + content: '\00d7'; +} + +.prompt-footer { + text-align: right; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + border-top-color: #222; + border-top-width: 1px; + + bottom: 0; +} + +.prompt-input { + float: left; + position: relative; + margin-left: -16px!important; + margin-top: -11px!important; +} + +.prompt-accept, .prompt-decline { + transition: all 0.1s; +} + +.prompt-accept { + background: #cc7b19!important; + margin-left: 5px!important; +} + +.prompt-accept:hover { + background: #e59029!important; +} + +.prompt-decline { + background: #ffffff40!important; +} + +.prompt-decline:hover { + background: #ffffff4d!important; +} + +[type="password"] ~ .hidden-help, .hide { + display: none; +} + +article { + color: #999; + background: rgba(0, 0, 0, 0.15) !important; + width: 35vw !important; + position: relative !important; + padding: 15px !important; +} + +summary, option { + margin-bottom: 20px !important; + padding-left: 0; + list-style: none !important; + margin-top: 0 !important; + color: #999; + cursor: pointer; +} + +select:not([multiple]) > option { + background: url(../img/noise.png), #3f4245 !important; +} + +details:last-child > summary { + margin-bottom: 0 !important; +} + +details[open] > summary, h1, h2, h3, h4, h5, h6 { + color: #cc7b19 !important; + text-shadow: 0 0 5px #000; + + z-index: 999; +} + +.test { + background: #197bcc !important; + font-family: monospace; +} + +.test:hover { + background: #298bdc !important; +} + +/* bbodine @CodePen - https://codepen.io/bbodine1/pen/novBm */ +.checkbox { + width: 80px; + height: 26px; + background: #000; + margin: 15px 0; + position: relative; + border-radius: 50px; + box-shadow: inset 0px 1px 1px rgba(0, 0, 0, 0.5), 0px 1px 0px rgba(255, 255, 255, 0.2); +} + +span.checkbox { + display: inline-block; + + margin: 0; + vertical-align: text-bottom; +} + +.checkbox::after { + content: 'OFF'; + color: #666; + position: absolute; + right: 10px; + z-index: 0; + font: 12px/26px Arial, sans-serif; + font-weight: bold; + text-shadow: 1px 1px 0px rgba(255, 255, 255, 0.15); +} + +.checkbox::before { + content: 'ON'; + color: #cc7b19; + position: absolute; + left: 10px; + z-index: 0; + font: 12px/26px Arial, sans-serif; + font-weight: bold; +} + +.checkbox[prompt-yes]::before { + content: attr(prompt-yes); + text-transform: uppercase; +} + +.checkbox[prompt-no]::after { + content: attr(prompt-no); + text-transform: uppercase; +} + +.checkbox[prompt-size="large"i]::before, .checkbox[prompt-size="large"i]::after { + font-size: 30px !important; +} + +.checkbox[prompt-size="medium"i]::before, .checkbox[prompt-size="medium"i]::after { + font-size: 21px !important; +} + +.checkbox[prompt-size="normal"i]::before, .checkbox[prompt-size="normal"i]::after { + font-size: 12px !important; +} + +.checkbox[prompt-size="small"i]::before, .checkbox[prompt-size="small"i]::after { + font-size: 6px !important; +} + +.checkbox[prompt="y/n"i]::before { + content: 'YES'; +} + +.checkbox[prompt="y/n"i]::after { + content: 'NO'; +} + +.checkbox label { + display: block; + width: 34px; + height: 20px; + cursor: pointer; + position: absolute; + top: 3px; + left: 3px; + z-index: 1; + background: #666; + border-radius: 50px; + transition: all 0.4s ease; + box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.3); +} + +.checkbox input[type=checkbox] { + visibility: hidden; +} + +.checkbox input[type=checkbox]:checked + label { + left: 43px; + background: #cc7b19; +} + +.checkbox[disabled] { + opacity: 0.25 !important; +} + +[white] { + color: #fff!important; +} + +[orange] { + color: #cc7b19!important; +} + +input[type="range"] { + appearance: none; + -webkit-appearance: none; + + background: #0004; + outline: none; + + height: 5px!important; + width: 83%!important; +} + +input[type="range"] + output { + display: inline-block; + position: relative; + width: 7%!important; + color: #cc7b19; + line-height: 20px; + text-align: center; + border-radius: 3px; + background: #000; + padding: 5px 10px; + margin-left: 8px; + vertical-align: sub; +} + +input[type="range"] + output::after { + position: absolute; + top: 8px; + left: -7px; + width: 0; + height: 0; + border-top: 7px solid transparent; + border-right: 7px solid #000; + border-bottom: 7px solid transparent; + content: ''; +} + +input[type="range"]::-webkit-slider-thumb { + appearance: none; + -webkit-appearance: none; + + background: #cc7b19; + border: 1px solid #cc7b19; + border-radius: 100%; + cursor: pointer; + + height: 32px; + width: 32px; +} + +input[type="range"]::-moz-range-thumb { + background: #cc7b19; + border: 1px solid #cc7b19; + border-radius: 100%; + cursor: pointer; + + height: 32px; + width: 32px; +} + +#version { + color: #fff; + + position: fixed; + right: 4px; + top: calc(100vh - 24px); +} + +[disabled], [disabled] * { + cursor: not-allowed!important; + color: #909090EE!important; +} + +[code], code { + border: 1px solid #FFF3; + font-family: monospace, console, consolas, system, arial !important; + + padding: 0 3px; +} + +*::-webkit-scrollbar { + width: 10px; +} + +*::-webkit-scrollbar-thumb { + min-height: 50px; + background: rgba(255, 255, 255, 0.15); + border: 2px solid rgba(0, 0, 0, 0); + border-radius: 8px; + background-clip: padding-box; +} + +*::-webkit-scrollbar-track { + background: url(../img/noise.png) fixed, url(../img/256.png) no-repeat fixed center, #3f4245 !important; +} + +*::placeholder { + color: #999!important; +} + +*::-moz-placeholder, *:-moz-placeholder { + color: #999; +} + +*::-webkit-input-placeholder { + color: #999; +} diff --git a/src/options/index.html b/src/options/index.html index 013d495..f91ef1c 100644 --- a/src/options/index.html +++ b/src/options/index.html @@ -1,571 +1,10 @@ - Web To Plex Options - + + + Web To Plex | Options + @@ -595,38 +34,38 @@

Plex Settings

- Login + Login

Login Settings

-
- +
+ Use your Plex token
How to find your Plex token.
-
+

— OR —

-
- - - +
+ Login with Plex + +
Your Plex username and password are never stored, only your Plex token.
Your username and password are used in order to get a token from Plex itself.
-
+

— OR —

-
- - - +
+ Attach to Ombi* + +
- Fill in Manager Settings with Ombi? +

Fill in Manager Settings with Ombi?

@@ -634,22 +73,21 @@

— OR —

Your Ombi Plex token will be shared by this extension.
-
-
- + +

- -
- Advanced + Advance

Plex Server Options

@@ -669,7 +107,7 @@

Manager Settings

- Ombi (Movies/TV Shows) + Ombi (Movies/TV Shows)

Connection Settings

@@ -708,10 +146,16 @@

Login (saved)

+

+
+ try out ombi +
+
+
- Watcher (Movies) + Watcher (Movies)

Connection Settings

@@ -764,10 +208,14 @@

Login (saved)

+

+
+ try out watcher +
- Radarr (Movies) + Radarr (Movies)

Connection Settings

@@ -783,7 +231,7 @@

Connection Settings

1. Go to Radarr | Settings | General
- 2. Click on "Show advanced," then copy/paste the "API Key."
+ 2. Click on "Show advance," then copy/paste the "API Key."
Such as aa756d33242f6g8ffbca2b3963586f21
@@ -809,7 +257,7 @@

Login (saved)

-
@@ -822,26 +270,30 @@

Login (saved)

+

+
+ try out radarr +
-
- Sonarr (TV Shows) +
+ CouchPotato (Movies)

Connection Settings

- - + +
- Such as https://example.com/sonarr or http://192.168.1.100:8989 + Such as https://example.com/couchpotato or http://192.168.1.100:5050
- - + +
- 1. Go to Sonarr | Settings | General
- 2. Click on "Show advanced," then copy/paste the "API Key."
+ 1. Go to CouchPotato | Settings
+ 2. Copy/Paste the "API Key."
Such as aa756d33242f6g8ffbca2b3963586f21
@@ -849,57 +301,126 @@

Connection Settings

Login (saved)

- -
Only use this if you setup a Sonarr username.
+ +
Only use this if you setup a CouchPotato username.
- -
Only use this if you setup a Sonarr password.
+ +
Only use this if you setup a CouchPotato password.
Your password will be hidden once saved.
-
-
This information will be used for Basic Access Authentication only. This will allow the extension to ask Sonarr for your list of shows, or to add to your list of shows.
- +
This information will be used for Basic Access Authentication only. This will allow the extension to ask Couchpotato for your list of videos, or to add to your list of videos.
- + +
+ +
+

+
+ try out couchpotato +
-
- CouchPotato (Movies/TV Shows) +
+ +
+ Medusa (TV Shows) + +

Connection Settings

+
+ + +
+ Such as https://example.com/medusa or http://192.168.1.100:8081 +
+
+ +
+ + +
+ 1. Go to Medusa | Settings | General | Interface | Web Interface
+ 2. Copy/Paste the "API key."
+ Such as aa756d33242f6g8ffbca2b3963586f21 +
+
+ +

Login (saved)

+
+ + +
+
+ + +
Your password will be hidden once saved.
+
+
This information will be used for Basic Access Authentication only. This will allow the extension to ask Medusa for your list of TV shows, or to add to your list of TV shows.
+
+ +
+ +
+ +
+ + +
+ This should be the same path (verbatim) used in Medusa. +
+
+
+ + +
+ + +
+

+
+ try out medusa +
+
+ +
+ Sonarr (TV Shows)

Connection Settings

- - + +
- Such as https://example.com/couchpotato or http://192.168.1.100:5050 + Such as https://example.com/sonarr or http://192.168.1.100:8989
- - + +
- 1. Go to CouchPotato | Settings
- 2. Copy/Paste the "API Key."
+ 1. Go to Sonarr | Settings | General
+ 2. Click on "Show advance," then copy/paste the "API Key."
Such as aa756d33242f6g8ffbca2b3963586f21
@@ -907,32 +428,113 @@

Connection Settings

Login (saved)

- -
Only use this if you setup a CouchPotato username.
+ +
Only use this if you setup a Sonarr username.
- -
Only use this if you setup a CouchPotato password.
+ +
Only use this if you setup a Sonarr password.
Your password will be hidden once saved.
+
+
This information will be used for Basic Access Authentication only. This will allow the extension to ask Sonarr for your list of TV shows, or to add to your list of TV shows.
-
This information will be used for Basic Access Authentication only. This will allow the extension to ask Couchpotato for your list of videos, or to add to your list of videos.
+
- +

+
+ try out sonarr +
+ +
+ Sick Beard (TV Shows) + +

Connection Settings

+
+ + +
+ Such as https://example.com/sickBeard or http://192.168.1.100:8081 +
+
+ +
+ + +
+ 1. Go to Sick Beard | Config | General | API
+ — a. Ensure the checkbox "Enable API" is enabled
+ — b. Press the "Generate" button
+ 2. Copy/Paste the "API Key."
+ Such as aa756d33242f6g8ffbca2b3963586f21 +
+
+ +

Login (saved)

+
+ + +
+
+ + +
Your password will be hidden once saved.
+
+
This information will be used for Basic Access Authentication only. This will allow the extension to ask Sick Beard for your list of TV shows, or to add to your list of TV shows.
+
+ +
+ +
+ +
+ + +
+ This should be the same path (verbatim) used in Sick Beard. +
+
+
+ + +
+ + + +
+

+
+ try out Sick Beard +
+
@@ -940,13 +542,35 @@

Other Settings

- Plugins + Theme Settings + +

The Button

+
+ Where should the button be? +
+ + + + + +
+
+
+ +
+
+ Site Settings

Experimental Sites

-
+
+ +

Default Sites

+
+ +
@@ -956,18 +580,20 @@

Experimental Sites

Proxy Settings

- -
- - -
+

+ Force Secure Connections + + + + +

- If enabled, all insecure (HTTP) requests will be through an HTTPS proxy. + If enabled, all insecure (HTTP) requests will be through an HTTPS proxy.
- +

Proxy URL & Syntax

Please provide the URL of your proxy. @@ -975,7 +601,7 @@

Proxy Settings

If your proxy requires a special syntax, enter that information in as well.
    -
  • {url} OR {raw-url} — the raw, uneditied URL
  • +
  • {raw-url} OR {url} — the raw, uneditied URL
  • {enc-url} OR {encode-url} — an encoded URL
  • {b64-url} OR {base64-url} — a Base-64 encoded URL
@@ -983,26 +609,26 @@

Proxy Settings

- +

Proxy Headers

- If your proxy requires special headers, enter that information in here. + If your proxy requires special headers, enter that information in here.
-
-
- Advanced Settings - -

List Options

+
+
+ Media Settings
-

AutoGrab

-
- - -
+

+ Auto Grab + + + + +

When the user presses the Grab button, the extension should:
    @@ -1010,7 +636,7 @@

    AutoGrab

  • ASK user: Find items not on Plex, and grab what the user approves
-

Maximum AutoGrabs

+

Maximum Auto Grabs

@@ -1018,13 +644,76 @@

Maximum AutoGrabs


-

Search Options

-
-

Loose Searching

-
- - -
+
+

+ Prompt for Save Location + + + + +

+
+ When the user presses the Grab button should the save location be asked for? +
+ Only supports Medusa, Radarr, and Sonarr. +
+
+
+
+

+ Prompt for Quality + + + + +

+
+ When the user presses the Grab button should the quality be asked for? +
+ Only supports Medusa, Radarr, and Sonarr. +
+
+
+
+ +
+
+ Notification Settings +

+ Ignore Found Items + + + + +

+
+ When the user presses the Grab button and an item already exists, should the notification be ignored or not? +
+
+

+ Ignore Repetitive Notifications + + + + +

+
+ When the user presses the Grab button and there are several incoming notifications of similar information, should the notifications be ignored (after the first one) or not? +
+
+
+ +
+
+ Search Settings +
+

+ Loose Searching + + + + +


@@ -1035,19 +724,26 @@

Loose Searching

Higer sensitivity means more strict searches.
- +
-

Manager Searching

-
- - -
+

+ Manager Searching + + + + +

Allows the extension to use your manager(s) to find media.
- Currently supports: Ombi, Radarr, and Sonarr. + Currently supports: Medusa, Ombi, Radarr, and Sonarr.
-
+
+
+ +
+
+ Advance Settings

API Keys

@@ -1078,16 +774,31 @@

Configuration Data (Copy/Paste)


-

"Local Search" Cache

- +

Cached Data

+
- This will empty all of you "Local Search" results. + This will remove all of your Local Search, permission, and other cached data.
This action cannot be undone.
+
+

Developer Options

+
+

+ Developer Mode + + + + +

+ +
@@ -1138,16 +849,54 @@

External Links

Download, and/or find more information for Ombi. + +
+ +
+ Download, and/or find more information for Medusa. +
+
+ +
+ +
+ Download, and/or find more information for Sick Beard. +
+

-
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/src/options/index.js b/src/options/index.js index dcda978..f383d32 100644 --- a/src/options/index.js +++ b/src/options/index.js @@ -1,89 +1,212 @@ /* global parseXML */ /* Notes: - #1: See https://github.com/SpaceK33z/web-to-plex/commit/db01d1a83d32e4d73f2ea671f634e6cc5b4c0fe7 - #2: See https://github.com/SpaceK33z/web-to-plex/commit/27506b9a4c12496bd7aad6ee09deb8a5b9418cac - #3: See https://github.com/SpaceK33z/web-to-plex/issues/21 - #4: See https://github.com/SpaceK33z/web-to-plex/issues/61 + #1: See https://github.com/SpaceK33z/web-to-plex/commit/db01d1a83d32e4d73f2ea671f634e6cc5b4c0fe7 + #2: See https://github.com/SpaceK33z/web-to-plex/commit/27506b9a4c12496bd7aad6ee09deb8a5b9418cac + #3: See https://github.com/SpaceK33z/web-to-plex/issues/21 + #4: See https://github.com/SpaceK33z/web-to-plex/issues/61 */ -let NO_DEBUGGER = false; +let DEVELOPER_MODE; if(chrome.runtime.lastError) - /* Always causes errors on *nix machines, so just "poke" the errors here */ - chrome.runtime.lastError.message; + /* Always causes errors on *nix machines, so just "poke" the errors here */ + chrome.runtime.lastError.message; // FireFox doesn't support sync storage. const storage = (chrome.storage.sync || chrome.storage.local), - $$ = (selector, all) => (all? document.querySelectorAll(selector): document.querySelector(selector)), - __servers__ = $$('#plex_servers'), - __watcher_qualityProfile__ = $$(`[data-option="watcherQualityProfileId"]`), - __watcher_storagePath__ = $$(`[data-option="watcherStoragePath"]`), - __radarr_qualityProfile__ = $$(`[data-option="radarrQualityProfileId"]`), - __radarr_storagePath__ = $$(`[data-option="radarrStoragePath"]`), - __sonarr_qualityProfile__ = $$(`[data-option="sonarrQualityProfileId"]`), - __sonarr_storagePath__ = $$(`[data-option="sonarrStoragePath"]`), - __save__ = $$('#save'), - __options__ = [ - 'plexURL', - 'plexToken', - 'UseOmbi', - 'couchpotatoURLRoot', - 'couchpotatoToken', - 'couchpotatoBasicAuthUsername', - 'couchpotatoBasicAuthPassword', -// 'couchpotatoQualityProfileId', - 'watcherURLRoot', - 'watcherToken', - 'watcherBasicAuthUsername', - 'watcherBasicAuthPassword', - 'watcherStoragePath', - 'watcherQualityProfileId', - 'radarrURLRoot', - 'radarrToken', - 'radarrBasicAuthUsername', - 'radarrBasicAuthPassword', - 'radarrStoragePath', - 'radarrQualityProfileId', - 'sonarrURLRoot', - 'sonarrToken', - 'sonarrBasicAuthUsername', - 'sonarrBasicAuthPassword', - 'sonarrStoragePath', - 'sonarrQualityProfileId', - 'ombiURLRoot', - 'ombiToken', - - // Connection settings - 'UseProxy', - 'ProxyURL', - 'ProxyHeaders', - - // Advance Settings - 'OMDbAPI', - 'TMDbAPI', - 'UseAutoGrab', - 'AutoGrabLimit', - 'UseLoose', - 'UseLooseScore', - 'ManagerSearch', - - // Plugins - End of file, before "let empty = ..." - 'plugin_toloka', - 'plugin_shanaproject', - 'plugin_myanimelist', - 'plugin_myshows', - ]; + $ = (selector, all) => (all? document.querySelectorAll(selector): document.querySelector(selector)), + __servers__ = $('[data-option="preferredServer"]'), + __sickBeard_qualityProfile__ = $(`[data-option="sickBeardQualityProfileId"]`), + __sickBeard_storagePath__ = $(`[data-option="sickBeardStoragePath"]`), + __medusa_qualityProfile__ = $(`[data-option="medusaQualityProfileId"]`), + __medusa_storagePath__ = $(`[data-option="medusaStoragePath"]`), + __watcher_qualityProfile__ = $(`[data-option="watcherQualityProfileId"]`), + __watcher_storagePath__ = $(`[data-option="watcherStoragePath"]`), + __radarr_qualityProfile__ = $(`[data-option="radarrQualityProfileId"]`), + __radarr_storagePath__ = $(`[data-option="radarrStoragePath"]`), + __sonarr_qualityProfile__ = $(`[data-option="sonarrQualityProfileId"]`), + __sonarr_storagePath__ = $(`[data-option="sonarrStoragePath"]`), + __save__ = $('#save'), + __options__ = [ + /* Plex Settings */ + 'plexURL', + 'plexToken', + 'UseOmbi', + 'preferredServer', + + /* Manager Settings */ + // Ombi + 'usingOmbi', + 'ombiURLRoot', + 'ombiToken', + + // Medusa + 'usingMedusa', + 'medusaURLRoot', + 'medusaToken', + 'medusaBasicAuthUsername', + 'medusaBasicAuthPassword', + 'medusaStoragePath', + 'medusaQualityProfileId', + + // Watcher + 'usingWatcher', + 'watcherURLRoot', + 'watcherToken', + 'watcherBasicAuthUsername', + 'watcherBasicAuthPassword', + 'watcherStoragePath', + 'watcherQualityProfileId', + + // Radarr + 'usingRadarr', + 'radarrURLRoot', + 'radarrToken', + 'radarrBasicAuthUsername', + 'radarrBasicAuthPassword', + 'radarrStoragePath', + 'radarrQualityProfileId', + + // Sonarr + 'usingSonarr', + 'sonarrURLRoot', + 'sonarrToken', + 'sonarrBasicAuthUsername', + 'sonarrBasicAuthPassword', + 'sonarrStoragePath', + 'sonarrQualityProfileId', + + // Sick Beard + 'usingSickBeard', + 'sickBeardURLRoot', + 'sickBeardToken', + 'sickBeardBasicAuthUsername', + 'sickBeardBasicAuthPassword', + 'sickBeardStoragePath', + 'sickBeardQualityProfileId', + + // CouchPotato + 'enableCouchPotato', + 'usingCouchPotato', + 'couchpotatoURLRoot', + 'couchpotatoToken', + 'couchpotatoBasicAuthUsername', + 'couchpotatoBasicAuthPassword', + // 'couchpotatoQualityProfileId', + + /* Other Settings */ + // Connection settings + 'UseProxy', + 'ProxyURL', + 'ProxyHeaders', + + // Media settings + 'UseAutoGrab', + 'AutoGrabLimit', + 'PromptLocation', + 'PromptQuality', + + // Notification Settings + 'NotifyNewOnly', + 'NotifyOnlyOnce', + + // Search Settings + 'UseLoose', + 'UseLooseScore', + 'ManagerSearch', + + // Advance Settings + 'OMDbAPI', + 'TMDbAPI', + 'DeveloperMode', + + // Hidden values + 'watcherQualities', + 'radarrQualities', + 'sonarrQualities', + 'medusaQualities', + 'sickBeardQualities', + 'watcherStoragePaths', + 'radarrStoragePaths', + 'sonarrStoragePaths', + 'medusaStoragePaths', + 'sickBeardStoragePaths', + '__radarrQuality', + '__sonarrQuality', + '__medusaQuality', + '__sickBeardQuality', + '__radarrStoragePath', + '__sonarrStoragePath', + '__medusaStoragePath', + '__sickBeardStoragePath', + '__domains', + '__caught', + '__theme', + + // Builtins + 'builtin_allocine', + 'builtin_amazon', + 'builtin_couchpotato', + 'builtin_fandango', + 'builtin_flickmetrix', + 'builtin_google', + 'builtin_googleplay', + 'builtin_hulu', + 'builtin_imdb', + 'builtin_justwatch', + 'builtin_letterboxd', + 'builtin_metacritic', + 'builtin_moviemeter', + 'builtin_movieo', + 'builtin_netflix', + 'builtin_plex', + 'builtin_rottentomatoes', + 'builtin_shanaproject', + 'builtin_showrss', + 'builtin_tmdb', + 'builtin_tvmaze', + 'builtin_tvdb', + 'builtin_trakt', + 'builtin_vrv', + 'builtin_verizon', + 'builtin_vudu', + 'builtin_vumoo', + 'builtin_youtube', + 'builtin_itunes', + 'builtin_gostream', + 'builtin_tubi', + 'builtin_webtoplex', + + // Plugins - End of file, before "let empty = ..." + 'plugin_toloka', + 'plugin_shanaproject', + 'plugin_myanimelist', + 'plugin_myshows', + 'plugin_indomovie', + 'plugin_redbox', + 'plugin_kitsu', + ]; let PlexServers = [], - ServerID = null, - ClientID = null, - manifest = chrome.runtime.getManifest(), - terminal = // See #3 - NO_DEBUGGER? - { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m }: - console; - -chrome.manifest = chrome.runtime.getManifest(); + ServerID = null, + ClientID = null, + manifest = chrome.runtime.getManifest(), + terminal = // See #3 + (DEVELOPER_MODE = $('[data-option="DeveloperMode"]').checked)? + console: + { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m }; + +chrome.manifest = manifest; + +// Not really important variables +// The "caught" IDs (already asked for in managers) +let __caught = { + imdb: [], + tmdb: [], + tvdb: [], +}, +// The theme classes + __theme = []; // create and/or queue a notification // state = "error" - red @@ -91,186 +214,186 @@ chrome.manifest = chrome.runtime.getManifest(); // state = "info" - grey // anything else for state will show as orange class Notification { - constructor(state, text, timeout = 7000, callback = () => {}, requiresClick = true) { - let queue = (Notification.queue = Notification.queue || { list: [] }), - last = queue.list[queue.list.length - 1]; - - if (last && last.done === false) - return (last => setTimeout(() => new Notification(state, text, timeout, callback, requiresClick), +(new Date) - last.start))(last); - - let element = document.furnish(`div.notification.${state}`, { - onclick: event => { - let notification = Notification.queue[event.target.id], - element = notification.element; - - notification.done = true; - Notification.queue.list.splice(notification.index, 1); - clearTimeout(notification.job); - element.remove(); - - let removed = delete Notification.queue[notification.id]; - - return (event.requiresClick)? null: notification.callback(removed); - } - }, text); - - queue[element.id = +(new Date)] = { - start: +element.id, - stop: +element.id + timeout, - span: +timeout, - done: false, - index: queue.list.length, - job: setTimeout(() => element.onclick({ target: element, requiresClick }), timeout), - id: +element.id, - callback, element - }; - queue.list.push(queue[element.id]); - - document.body.appendChild(element); - - return queue[element.id]; - } + constructor(state, text, timeout = 7000, callback = () => {}, requiresClick = true) { + let queue = (Notification.queue = Notification.queue || { list: [] }), + last = queue.list[queue.list.length - 1]; + + if (last && last.done === false) + return (last => setTimeout(() => new Notification(state, text, timeout, callback, requiresClick), +(new Date) - last.start))(last); + + let element = document.furnish(`div.notification.${state}`, { + onclick: event => { + let notification = Notification.queue[event.target.id], + element = notification.element; + + notification.done = true; + Notification.queue.list.splice(notification.index, 1); + clearTimeout(notification.job); + element.remove(); + + let removed = delete Notification.queue[notification.id]; + + return (event.requiresClick)? null: notification.callback(removed); + } + }, text); + + queue[element.id = +(new Date)] = { + start: +element.id, + stop: +element.id + timeout, + span: +timeout, + done: false, + index: queue.list.length, + job: setTimeout(() => element.onclick({ target: element, requiresClick }), timeout), + id: +element.id, + callback, element + }; + queue.list.push(queue[element.id]); + + document.body.appendChild(element); + + return queue[element.id]; + } } class Prompt { - constructor(type, options, callback = () => {}, container = document.body) { - let prompt, remove, create, - array = (options instanceof Array? options: [].slice.call(options)), - data = [...array]; - - switch(type) { - /* Allows the user to add and remove items from a list */ - case 'prompt': - case 'input': - remove = element => { - let prompter = document.querySelector('.prompt'), - header = document.querySelector('.prompt-header'), - counter = document.querySelector('.prompt-options'); - - if(element === true) - return prompter.remove(); - else - element.remove(); - - data.splice(+element.value, 1, null); - header.innerText = 'Approve ' + counter.children.length + (counter.children.length == 1?' item': ' items'); - }; - - prompt = document.furnish('div.prompt', {}, - document.furnish('div.prompt-body', {}, - // The prompt's title - document.furnish('h1.prompt-header', {}, 'Approve ' + array.length + (array.length == 1? ' item': ' items')), - - // The prompt's items - document.furnish('div.prompt-options', {}, - ...(create = ITEMS => { - let elements = []; - - for(let index = 0, length = ITEMS.length, ITEM; index < length; index++) - ITEM = ITEMS[index], - elements.push( - document.furnish('li.prompt-option.mutable', { value: index }, - JSON.stringify(ITEM), - document.furnish('button', { title: 'Remove', onclick: event => { remove(event.target.parentElement); event.target.remove() } }) - ) - ); - - return elements - })(array) - ), - - // The engagers - document.furnish('div.prompt-footer', {}, - document.furnish('input.prompt-input[type=text]', { placeholder: 'Add an item (enter to add)', onkeydown: event => { - let self = event.target; - - if (event.keyCode === 13) { - event.preventDefault(); - remove(true); - - let value = self.value; - - try { - value = JSON.parse(value); - } catch(error) { - /* Suppress input errors */ - } - - new Prompt(type, [value, ...data.filter(value => value !== null && value !== undefined)], callback, container); - } - } }), - document.furnish('button.prompt-decline', { onclick: event => { remove(true); callback([]) } }, 'Close'), - document.furnish('button.prompt-accept', { onclick: event => { remove(true); new Prompt(type, options, callback, container) } }, 'Reset'), - document.furnish('button.prompt-accept', { onclick: event => { remove(true); callback(data.filter(value => value !== null && value !== undefined)) } }, 'Continue') - ) - ) - ); - break; - - /* Allows the user to remove predetermined items */ - case 'select': - remove = element => { - let prompter = document.querySelector('.prompt'), - header = document.querySelector('.prompt-header'), - counter = document.querySelector('.prompt-options'); - - if(element === true) - return prompter.remove(); - else - element.remove(); - - data.splice(+element.value, 1, null); - header.innerText = 'Approve ' + counter.children.length + (counter.children.length == 1?' item': ' items'); - }; - - prompt = document.furnish('div.prompt', {}, - document.furnish('div.prompt-body', {}, - // The prompt's title - document.furnish('h1.prompt-header', {}, 'Approve ' + array.length + (array.length == 1? ' item': ' items')), - - // The prompt's items - document.furnish('div.prompt-options', {}, - ...(create = ITEMS => { - let elements = []; - - for(let index = 0, length = ITEMS.length, ITEM; index < length; index++) - ITEM = ITEMS[index], - elements.push( - document.furnish('li.prompt-option.mutable', { value: index }, - JSON.stringify(ITEM), - document.furnish('button', { title: 'Remove', onclick: event => { remove(event.target.parentElement); event.target.remove() } }) - ) - ); - - return elements - })(array) - ), - - // The engagers - document.furnish('div.prompt-footer', {}, - document.furnish('button.prompt-decline', { onclick: event => { remove(true); callback([]) } }, 'Close'), - document.furnish('button.prompt-accept', { onclick: event => { remove(true); new Prompt(type, options, callback, container) } }, 'Reset'), - document.furnish('button.prompt-accept', { onclick: event => { remove(true); callback(data.filter(value => value !== null && value !== undefined)) } }, 'Continue') - ) - ) - ); - break; - - default: - return terminal.warn(`Unknown prompt type "${ type }"`); - break; - } - - return container.append(prompt), prompt; - } + constructor(type, options, callback = () => {}, container = document.body) { + let prompt, remove, create, + array = (options instanceof Array? options: [].slice.call(options)), + data = [...array]; + + switch(type) { + /* Allows the user to add and remove items from a list */ + case 'prompt': + case 'input': + remove = element => { + let prompter = document.querySelector('.prompt'), + header = document.querySelector('.prompt-header'), + counter = document.querySelector('.prompt-options'); + + if(element === true) + return prompter.remove(); + else + element.remove(); + + data.splice(+element.value, 1, null); + header.innerText = 'Approve ' + counter.children.length + (counter.children.length == 1?' item': ' items'); + }; + + prompt = document.furnish('div.prompt', {}, + document.furnish('div.prompt-body', {}, + // The prompt's title + document.furnish('h1.prompt-header', {}, 'Approve ' + array.length + (array.length == 1? ' item': ' items')), + + // The prompt's items + document.furnish('div.prompt-options', {}, + ...(create = ITEMS => { + let elements = []; + + for(let index = 0, length = ITEMS.length, ITEM; index < length; index++) + ITEM = ITEMS[index], + elements.push( + document.furnish('li.prompt-option.mutable', { value: index }, + JSON.stringify(ITEM), + document.furnish('button', { title: 'Remove', onclick: event => { remove(event.target.parentElement); event.target.remove() } }) + ) + ); + + return elements + })(array) + ), + + // The engagers + document.furnish('div.prompt-footer', {}, + document.furnish('input.prompt-input[type=text]', { placeholder: 'Add an item (enter to add)', onkeydown: event => { + let self = event.target; + + if (event.keyCode === 13) { + event.preventDefault(); + remove(true); + + let value = self.value; + + try { + value = JSON.parse(value); + } catch(error) { + /* Suppress input errors */ + } + + new Prompt(type, [value, ...data.filter(value => value !== null && value !== undefined)], callback, container); + } + } }), + document.furnish('button.prompt-decline', { onclick: event => { remove(true); callback([]) } }, 'Close'), + document.furnish('button.prompt-accept', { onclick: event => { remove(true); new Prompt(type, options, callback, container) } }, 'Reset'), + document.furnish('button.prompt-accept', { onclick: event => { remove(true); callback(data.filter(value => value !== null && value !== undefined)) } }, 'Continue') + ) + ) + ); + break; + + /* Allows the user to remove predetermined items */ + case 'select': + remove = element => { + let prompter = document.querySelector('.prompt'), + header = document.querySelector('.prompt-header'), + counter = document.querySelector('.prompt-options'); + + if(element === true) + return prompter.remove(); + else + element.remove(); + + data.splice(+element.value, 1, null); + header.innerText = 'Approve ' + counter.children.length + (counter.children.length == 1?' item': ' items'); + }; + + prompt = document.furnish('div.prompt', {}, + document.furnish('div.prompt-body', {}, + // The prompt's title + document.furnish('h1.prompt-header', {}, 'Approve ' + array.length + (array.length == 1? ' item': ' items')), + + // The prompt's items + document.furnish('div.prompt-options', {}, + ...(create = ITEMS => { + let elements = []; + + for(let index = 0, length = ITEMS.length, ITEM; index < length; index++) + ITEM = ITEMS[index], + elements.push( + document.furnish('li.prompt-option.mutable', { value: index }, + JSON.stringify(ITEM), + document.furnish('button', { title: 'Remove', onclick: event => { remove(event.target.parentElement); event.target.remove() } }) + ) + ); + + return elements + })(array) + ), + + // The engagers + document.furnish('div.prompt-footer', {}, + document.furnish('button.prompt-decline', { onclick: event => { remove(true); callback([]) } }, 'Close'), + document.furnish('button.prompt-accept', { onclick: event => { remove(true); new Prompt(type, options, callback, container) } }, 'Reset'), + document.furnish('button.prompt-accept', { onclick: event => { remove(true); callback(data.filter(value => value !== null && value !== undefined)) } }, 'Continue') + ) + ) + ); + break; + + default: + return terminal.warn(`Unknown prompt type "${ type }"`); + break; + } + + return container.append(prompt), prompt; + } } function load(name) { - return JSON.parse(localStorage.getItem(btoa(name))); + return JSON.parse(localStorage.getItem(btoa(name))); } function save(name, data) { - return localStorage.setItem(btoa(name), JSON.stringify(data)); + return localStorage.setItem(btoa(name), JSON.stringify(data)); } function getServers(plexToken) { @@ -279,60 +402,61 @@ function getServers(plexToken) { 'X-Plex-Token': plexToken } }) - .then(response => response.text()) - .then(xml => { - let data = parseXML(xml); + .then(response => response.text()) + .then(xml => { + let data = parseXML(xml); - if(/^\s*Invalid/i.test(data)) - return null; + if(/^\s*Invalid/i.test(data)) + return null; - return data.Device.filter(device => device.provides === 'server'); - }); + return data.Device.filter(device => !!~device.provides.split(',').indexOf('server')); + }); } /* See #1 */ function tryPlexLogin(username, password) { - let hash = btoa(`${username}:${password}`); - - return fetch(`https://plex.tv/users/sign_in.json`, { - method: 'POST', - headers: { - 'X-Plex-Product': 'Web to Plex', - 'X-Plex-Version': manifest.version, - 'X-Plex-Client-Identifier': ClientID, - 'Authorization': `Basic ${ hash }` - } - }) - .then(response => response.json()); + let hash = btoa(`${username}:${password}`); + + return fetch(`https://plex.tv/users/sign_in.json`, { + method: 'POST', + headers: { + 'X-Plex-Product': 'Web to Plex', + 'X-Plex-Version': manifest.version, + 'X-Plex-Client-Identifier': ClientID, + 'Authorization': `Basic ${ hash }` + } + }) + .then(response => response.json()); } function performPlexLogin() { - let u = $$('#plex_username').value, - p = $$('#plex_password').value, - s = $$('#plex_test_status'); + let u = $('#plex_username').value, + p = $('#plex_password').value, + s = $('#plex_test_status'); - s.title = ''; - __servers__.innerHTML = ''; - __save__.disabled = true; + s.title = ''; + __servers__.innerHTML = ''; + __save__.disabled = true; - tryPlexLogin(u, p) - .then(response => { - if(response.error) - return s.title = 'Invalid login information', null; + tryPlexLogin(u, p) + .then(response => { + if(response.error) + return s.title = 'Invalid login information', null; - if(response.user) { - let t = $$('#plex_token'); + if(response.user) { + let t = $('#plex_token'); - ClientID = t.value = t.textContent = response.user.authToken; + ClientID = t.value = t.textContent = response.user.authToken; - return performPlexTest(); - } - }); + return performPlexTest(); + } + }); } function performPlexTest(ServerID) { - let plexToken = $$('#plex_token').value, - teststatus = $$('#plex_test_status'); + let plexToken = $('#plex_token').value, + teststatus = $('#plex_test_status'), + inusestataus = [...$('[in-use="plex_token"]', true)]; __save__.disabled = true; __servers__.innerHTML = ''; @@ -340,26 +464,28 @@ function performPlexTest(ServerID) { getServers(plexToken).then(servers => { PlexServers = servers || []; - teststatus.textContent = '!'; + teststatus.textContent = '!'; + inusestataus.map(e => e.setAttribute('in-use', false)); if(!servers) - return teststatus.title = 'Failed to communicate with Plex', teststatus.classList = false; + return teststatus.title = 'Failed to communicate with Plex', teststatus.classList = false; + inusestataus.map(e => e.setAttribute('in-use', true)); __save__.disabled = false; - teststatus.classList = true; + teststatus.classList = true; (servers = [{ sourceTitle: 'GitHub', clientIdentifier: '', name: 'No Plex Server' }, ...servers]).forEach(server => { let $option = document.createElement('option'), - source = server.sourceTitle; + source = server.sourceTitle; $option.value = server.clientIdentifier; - $option.textContent = `${ server.name } ${ source ? `(${ source })` : '' }`; + $option.textContent = `${ server.name }${ source ? ` \u2014 ${ source }` : '' }`; __servers__.appendChild($option); }); - if(ServerID) { + if(ServerID) { __servers__.value = ServerID; - } + } }); } @@ -381,187 +507,217 @@ function getPlexConnections(server) { function getOptionValues() { let options = {}; + for(let key in __caught) + __caught[key] = __caught[key].filter(id => id).slice(0, 100); + + __theme = __theme.filter(v => v); + + $('[data-option="__caught"i]').value = JSON.stringify(__caught); + $('[data-option="__theme"i]').value = JSON.stringify(__theme); + __options__.forEach(option => { - let element = $$( + let element = $( `[data-option="${ option }"]` ); - if(element) { - if(element.type == 'checkbox') - options[option] = element.checked || element.getAttribute('checked') == "true"; - else - options[option] = element.value; - } + if(element) { + if(element.type == 'checkbox') + options[option] = element.checked || element.getAttribute('save') == "true"; + else + options[option] = element.value; + } }); return options; } function performOmbiLogin() { - let l = $$('#ombi_url').value, - a = $$('#ombi_api').value, - s = $$('#plex_test_status'), - e = ($0, $1, $$, $_) => ($1 + (/\\/.test($_)? '\\': '/')); - - l = l - .replace(/([^\\\/])$/, e) - .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); - s.title = ''; - __servers__.innerHTML = ''; - __save__.disabled = true; - - let APIURL = `${ l }api/v1/`, - headers = { headers: { apikey: a, accept: 'application/json' } }; - - fetch(`${ APIURL }Settings/plex`, headers) - .then( response => response.json() ) - .then( json => { - /* Get Plex's details first. If it's disabled, or non-existent, then exit */ - /* Swagger API says "enable", but we'll go with "enabled" */ - if(json && (json.enable || json.enabled)) { - let t = $$('#plex_token'), - s = $$('#plex_servers'), - u = $$('[data-option="UseOmbi"]'); - - json = (json && json.servers.length? json.servers[0]: {}); - - let name = json.name, // people friendly server name - token = json.plexAuthToken, // the auth token - uuid = json.machineIdentifier, // the machine ID - url = json.ip; // the Plex URL used - - url = url.replace(/(?:[^\/]+\/\/)?([^\/]+)\/?/, `http${ json.ssl? 's': '' }://$1:${ json.port }/`); - - ClientID = t.value = t.textContent = token; - ServerID = s.value = uuid; - s.innerHTML = ``; - - /* Now we can fill in the other details */ - if(u.checked) { - // Ombi - let L = $$('[data-option="ombiURLRoot"]'), - A = $$('[data-option="ombiToken"]'); - - L.value = L.textContent = l; - A.value = A.textContent = a; - - new Notification('update', 'Filled in Ombi', 3000); - - // CouchPotato - fetch(`${ APIURL }Settings/CouchPotato`, headers) - .then( data => data.json() ) - .then( json => { - if(!json || (!json.enabled && !json.enable)) return; - - let k = $$('[data-option="couchpotatoToken"]'), - K = $$('[data-option="couchpotatoURLRoot"]'); - - k.value = k.textContent = json.apiKey; - K.value = K.textContent = json.ip.replace(/(?:[^\/]+\/\/)?([^\/]+)\/?/, `http${ json.ssl? 's': '' }://$1:${ json.port }/`); - - new Notification('update', 'Filled in CouchPotato', 3000); - } ) - .catch( error => { new Notification('error', 'Error getting CouchPotato details from Ombi'); throw error } ); - - // Radarr - fetch(`${ APIURL }Settings/radarr`, headers) - .then( data => data.json() ) - .then( json => { - if(!json || (!json.enabled && !json.enable)) return; - - let k = $$('[data-option="radarrToken"]'), - K = $$('[data-option="radarrURLRoot"]'), - q = $$('[data-option="radarrQualityProfileId"]'), - Q = $$('[data-option="radarrStoragePath"]'), - _q, _Q; - - k.value = k.textContent = json.apiKey; - K.value = K.textContent = json.ip.replace(/(?:[^\/]+\/\/)?([^\/]+)\/?/, `http${ json.ssl? 's': '' }://$1:${ json.port }/`); - q.value = _q = json.defaultQualityProfile; - Q.value = _Q = json.defaultRootPath; - - q.innerHTML = ``; - Q.innerHTML = ``; - - new Notification('update', 'Filled in Radarr', 3000); - } ) - .catch( error => { new Notification('error', 'Error getting Radarr details from Ombi'); throw error } ); - - // Sonarr - fetch(`${ APIURL }Settings/sonarr`, headers) - .then( data => data.json() ) - .then( json => { - if(!json || (!json.enabled && !json.enable)) return; - - let k = $$('[data-option="sonarrToken"]'), - K = $$('[data-option="sonarrURLRoot"]'), - q = $$('[data-option="sonarrQualityProfileId"]'), - Q = $$('[data-option="sonarrStoragePath"]'), - _q, _Q; - - k.value = k.textContent = json.apiKey; - K.value = K.textContent = json.ip.replace(/(?:[^\/]+\/\/)?([^\/]+)\/?/, `http${ json.ssl? 's': '' }://$1:${ json.port }/`); - q.value = _q = json.qualityProfile; - Q.value = _Q = json.rootPath; - - q.innerHTML = ``; - Q.innerHTML = ``; - - new Notification('update', 'Filled in Sonarr', 3000); - } ) - .catch( error => { new Notification('error', 'Error getting Sonarr details from Ombi'); throw error } ); - } - - __save__.disabled = false; - } else { - /* Plex either doesn't exist, or is disabled */ - new Notification('error', 'Error getting Plex details from Ombi'); - } - } ) - .catch( error => { new Notification('error', error); throw error } ); + let l = $('#ombi_url').value, + a = $('#ombi_api').value, + s = $('#plex_test_status'), + e = ($0, $1, $$, $_) => ($1 + (/\\/.test($_)? '\\': '/')); + + l = l + .replace(/([^\\\/])$/, e) + .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); + s.title = ''; + __servers__.innerHTML = ''; + __save__.disabled = true; + + let APIURL = `${ l }api/v1/`, + headers = { headers: { apikey: a, accept: 'application/json' } }; + + fetch(`${ APIURL }Settings/plex`, headers) + .then( response => response.json() ) + .then( json => { + /* Get Plex's details first. If it's disabled, or non-existent, then exit */ + /* Swagger API says "enable", but we'll go with "enabled" */ + if(json && (json.enable || json.enabled)) { + let t = $('#plex_token'), + u = $('[data-option="UseOmbi"]'), + s = __servers__; + + json = (json && json.servers.length? json.servers[0]: {}); + + let name = json.name, // people friendly server name + token = json.plexAuthToken, // the auth token + uuid = json.machineIdentifier, // the machine ID + url = json.ip; // the Plex URL used + + url = url.replace(/(?:[^\/]+\/\/)?([^\/]+)\/?/, `http${ json.ssl? 's': '' }://$1:${ json.port }/`); + + ClientID = t.value = t.textContent = token; + ServerID = s.value = uuid; + s.innerHTML = ``; + + /* Now we can fill in the other details */ + if(u.checked) { + // Ombi + let L = $('[data-option="ombiURLRoot"]'), + A = $('[data-option="ombiToken"]'); + + L.value = L.textContent = l; + A.value = A.textContent = a; + + new Notification('update', 'Filled in Ombi', 3000); + + // CouchPotato + fetch(`${ APIURL }Settings/CouchPotato`, headers) + .then( data => data.json() ) + .then( json => { + if(!json || (!json.enabled && !json.enable)) return; + + let k = $('[data-option="couchpotatoToken"]'), + K = $('[data-option="couchpotatoURLRoot"]'); + + k.value = k.textContent = json.apiKey; + K.value = K.textContent = json.ip.replace(/(?:[^\/]+\/\/)?([^\/]+)\/?/, `http${ json.ssl? 's': '' }://$1:${ json.port }/`); + + new Notification('update', 'Filled in CouchPotato', 3000); + } ) + .catch( error => { new Notification('error', 'Error getting CouchPotato details from Ombi'); throw error } ); + + // Radarr + fetch(`${ APIURL }Settings/radarr`, headers) + .then( data => data.json() ) + .then( json => { + if(!json || (!json.enabled && !json.enable)) return; + + let k = $('[data-option="radarrToken"]'), + K = $('[data-option="radarrURLRoot"]'), + q = $('[data-option="radarrQualityProfileId"]'), + Q = $('[data-option="radarrStoragePath"]'), + _q, _Q; + + k.value = k.textContent = json.apiKey; + K.value = K.textContent = json.ip.replace(/(?:[^\/]+\/\/)?([^\/]+)\/?/, `http${ json.ssl? 's': '' }://$1:${ json.port }/`); + q.value = _q = json.defaultQualityProfile; + Q.value = _Q = json.defaultRootPath; + + q.innerHTML = ``; + Q.innerHTML = ``; + + new Notification('update', 'Filled in Radarr', 3000); + } ) + .catch( error => { new Notification('error', 'Error getting Radarr details from Ombi'); throw error } ); + + // Sonarr + fetch(`${ APIURL }Settings/sonarr`, headers) + .then( data => data.json() ) + .then( json => { + if(!json || (!json.enabled && !json.enable)) return; + + let k = $('[data-option="sonarrToken"]'), + K = $('[data-option="sonarrURLRoot"]'), + q = $('[data-option="sonarrQualityProfileId"]'), + Q = $('[data-option="sonarrStoragePath"]'), + _q, _Q; + + k.value = k.textContent = json.apiKey; + K.value = K.textContent = json.ip.replace(/(?:[^\/]+\/\/)?([^\/]+)\/?/, `http${ json.ssl? 's': '' }://$1:${ json.port }/`); + q.value = _q = json.qualityProfile; + Q.value = _Q = json.rootPath; + + q.innerHTML = ``; + Q.innerHTML = ``; + + new Notification('update', 'Filled in Sonarr', 3000); + } ) + .catch( error => { new Notification('error', 'Error getting Sonarr details from Ombi'); throw error } ); + } + + __save__.disabled = false; + } else { + /* Plex either doesn't exist, or is disabled */ + new Notification('error', 'Error getting Plex details from Ombi'); + } + } ) + .catch( error => { new Notification('error', error); throw error } ); } function performOmbiTest(refreshing = false) { - let options = getOptionValues(), - teststatus = $$('#ombi_test_status'), - path = $$('[data-option="ombiURLRoot"]'), - url, - headers = { headers: { apikey: options.ombiToken, accept: 'text/html' } }; - - teststatus.textContent = '?'; - options.ombiURLRoot = url = path.value = options.ombiURLRoot.replace(/^(\:\d+)/, 'localhost$1').replace(/^(?!^http(s)?:)/, 'http$1://').replace(/\/+$/, ''); - - let Get = () => - fetch(`${ url }/api/v1/Status`, headers) - .then( response => response.text() ) - .then( status => { - if (!status || !status.length) throw new Error('Unable to communicate with Ombi'); - - if ((status = +status) >= 200 && status < 400) { - teststatus.textContent = '!'; - teststatus.classList = 'true'; - } else { - teststatus.textContent = '!'; - teststatus.classList = 'false'; - - throw new Error(`Ombi error [${ status }]`); - } - } ) - .catch( error => { new Notification('error', error) } ); - - if(refreshing) - Get(); - else if(url && url.length) - requestURLPermissions(url + '/*', allowed => - (allowed)? - Get(): - new Notification('error', 'The user refused permission to access Ombi') - ); + let options = getOptionValues(), + teststatus = $('#ombi_test_status'), + path = $('[data-option="ombiURLRoot"]'), + url, + headers = { headers: { apikey: options.ombiToken, accept: 'text/html' } }, + enabled = $('#using-ombi'); + + teststatus.textContent = '?'; + options.ombiURLRoot = url = path.value = options.ombiURLRoot.replace(/^(\:\d+)/, 'localhost$1').replace(/^(?!^http(s)?:)/, 'http$1://').replace(/\/+$/, ''); + + let Get = () => { + fetch(`${ url }/api/v1/Request/movie`) + .then(r => r.json()) + .thne(json => { + json.map(item => { + __caught.imdb.push(item.imdbId); + __caught.tmdb.push(item.theMovieDbId); + }); + }); + + fetch(`${ url }/api/v1/Request/tv`) + .then(r => r.json()) + .thne(json => { + json.map(item => { + __caught.imdb.push(item.imdbId); + __caught.tvdb.push(item.tvDbId); + }); + }); + + fetch(`${ url }/api/v1/Status`, headers) + .then( response => response.text() ) + .then( status => { + if (!status || !status.length) throw new Error('Unable to communicate with Ombi'); + + if ((status = +status) >= 200 && status < 400) { + teststatus.textContent = '!'; + enabled.checked = teststatus.classList = true; + enabled.parentElement.removeAttribute('disabled'); + } else { + teststatus.textContent = '!'; + enabled.checked = teststatus.classList = false; + enabled.parentElement.setAttribute('disabled'); + + throw new Error(`Ombi error [${ status }]`); + } + } ) + .catch( error => { new Notification('error', error) } ); + } + + if(refreshing) + Get(); + else if(url && url.length) + requestURLPermissions(url + '/*', allowed => + (allowed)? + Get(): + new Notification('error', 'The user refused permission to access Ombi') + ); } function getWatcher(options, api = "getconfig") { - if(!options.watcherToken) - return new Notification('error', 'Invalid Watcher token'); + if(!options.watcherToken) + return new Notification('error', 'Invalid Watcher token'); let headers = { 'Accept': 'application/json', @@ -572,78 +728,95 @@ function getWatcher(options, api = "getconfig") { if(options.watcherBasicAuthUsername) headers.Authorization = `Basic ${ btoa(`${ options.watcherBasicAuthUsername }:${ options.watcherBasicAuthPassword }`) }`; - return fetch(`${ options.watcherURLRoot }/api/?apikey=${ options.watcherToken }&mode=${ api }&quality=${ options.watcherQualityProfileId || 'Default' }`, { headers }) - .then(response => response.json()) - .catch(error => { - return new Notification('error', 'Watcher failed to connect with error:' + String(error)), - []; - }); + return fetch(`${ options.watcherURLRoot }/api/?apikey=${ options.watcherToken }&mode=${ api }&quality=${ options.watcherQualityProfileId || 'Default' }`, { headers }) + .then(response => response.json()) + .catch(error => { + return new Notification('error', 'Watcher failed to connect with error:' + String(error)), + []; + }); } function performWatcherTest(QualityProfileID = 'Default', refreshing = false) { let options = getOptionValues(), - teststatus = $$('#watcher_test_status'), - path = $$('[data-option="watcherURLRoot"]'), - storagepath = __watcher_storagePath__, - quality = __watcher_qualityProfile__, - url; + teststatus = $('#watcher_test_status'), + path = $('[data-option="watcherURLRoot"]'), + storagepath = __watcher_storagePath__, + quality = __watcher_qualityProfile__, + url, + enabled = $('#using-watcher'); quality.innerHTML = ''; teststatus.textContent = '?'; - storagepath.value = '[Empty]'; - options.watcherURLRoot = url = path.value = options.watcherURLRoot.replace(/^(\:\d+)/, 'localhost$1').replace(/^(?!^http(s)?:)/, 'http$1://').replace(/\/+$/, ''); - - let Get = () => - getWatcher(options, 'getconfig').then(config => { - if(!config || !config.config) return new Notification('error', 'Failed to get Watcher configuration'); - - let names = config.config.Quality.Profiles, - path = config.config.Postprocessing.moverpath, - syntax = path.replace(/\/([\w\s\/\\\{\}]+)$/, '$1'), - profiles = []; - - path = path.replace(syntax, ''); - - for(let name in names) - profiles.push({ - id: name, - name - }); - - teststatus.textContent = '!'; - teststatus.classList = !!profiles.length; - - if(!profiles.length) - teststatus.title = 'Failed to communicate with Watcher'; - - profiles.forEach(profile => { - let option = document.createElement('option'); - - option.value = profile.id; - option.textContent = profile.name; - quality.appendChild(option); - }); - - // Because the was reset, the original value is lost. + if(QualityProfileID) + quality.value = QualityProfileID; + + storagepath.value = path || '[Default Location]'; + + $('[data-option="watcherStoragePaths"i]').value = JSON.stringify(path || { path: '[Default Location]', id: 0 }); + }); + } + + if(refreshing) + Get(); + else if(url && url.length) + requestURLPermissions(url + '/*', allowed => + (allowed)? + Get(): + new Notification('error', 'The user refused permission to access Watcher') + ); } function getRadarr(options, api = "profile") { - if(!options.radarrToken) - return new Notification('error', 'Invalid Radarr token'); + if(!options.radarrToken) + return new Notification('error', 'Invalid Radarr token'); let headers = { 'Accept': 'application/json', @@ -658,73 +831,92 @@ function getRadarr(options, api = "profile") { .then(response => response.json()) .catch(error => { return new Notification('error', 'Radarr failed to connect with error:' + String(error)), - []; + []; }); } function performRadarrTest(QualityProfileID, StoragePath, refreshing = false) { let options = getOptionValues(), - teststatus = $$('#radarr_test_status'), - path = $$('[data-option="radarrURLRoot"]'), - storagepath = __radarr_storagePath__, - quality = __radarr_qualityProfile__, - url; + teststatus = $('#radarr_test_status'), + path = $('[data-option="radarrURLRoot"]'), + storagepath = __radarr_storagePath__, + quality = __radarr_qualityProfile__, + url, + enabled = $('#using-radarr'); quality.innerHTML = ''; teststatus.textContent = '?'; - storagepath.textContent = ''; - options.radarrURLRoot = url = path.value = options.radarrURLRoot.replace(/^(\:\d+)/, 'localhost$1').replace(/^(?!^http(s)?:)/, 'http$1://').replace(/\/+$/, ''); - - let Get = () => { - getRadarr(options, 'profile').then(profiles => { - if(!profiles) return new Notification('error', 'Failed to get Radarr configuration'); - - teststatus.textContent = '!'; - teststatus.classList = !!profiles.length; - - if(!profiles.length) - teststatus.title = 'Failed to communicate with Radarr'; - - profiles.forEach(profile => { - let option = document.createElement('option'); - - option.value = profile.id; - option.textContent = profile.name; - quality.appendChild(option); - }); - - // Because the was reset, the original value is lost. - if(StoragePath) - storagepath.value = StoragePath; - }); - }; - - if(refreshing) - Get(); - else if(url && url.length) - requestURLPermissions(url + '/*', allowed => - (allowed)? - Get(): - new Notification('error', 'The user refused permission to access Radarr') - ); + storagepath.textContent = ''; + options.radarrURLRoot = url = path.value = options.radarrURLRoot.replace(/^(\:\d+)/, 'localhost$1').replace(/^(?!^http(s)?:)/, 'http$1://').replace(/\/+$/, ''); + + let Get = () => { + getRadarr(options, 'movie').then(movies => { + movies.map(movie => { + __caught.imdb.push(movie.imdbId); + __caught.tmdb.push(movie.tmdbId); + }); + }); + + getRadarr(options, 'profile').then(profiles => { + if(!profiles) return new Notification('error', 'Failed to get Radarr configuration'); + + teststatus.textContent = '!'; + teststatus.classList = enabled.checked = !!profiles.length; + + if(!profiles.length) + return teststatus.title = 'Failed to communicate with Radarr'; + enabled.parentElement.removeAttribute('disabled'); + + let qualities = []; + profiles.forEach(profile => { + let option = document.createElement('option'); + let { id, name } = profile; + + option.value = id; + option.textContent = name; + qualities.push({ id, name }); + quality.appendChild(option); + }); + + $('[data-option="radarrQualities"i]').value = JSON.stringify(qualities); + + // Because the was reset, the original value is lost. + if(StoragePath) { + storagepath.value = StoragePath; + $('[data-option="__radarrStoragePath"i]').value = StoragePaths.indexOf(StoragePath.replace(/\\/g, '/')) + 1; + } + }); + }; + + if(refreshing) + Get(); + else if(url && url.length) + requestURLPermissions(url + '/*', allowed => + (allowed)? + Get(): + new Notification('error', 'The user refused permission to access Radarr') + ); } function getSonarr(options, api = "profile") { - if(!options.sonarrToken) - return new Notification('error', 'Invalid Sonarr token'); + if(!options.sonarrToken) + return new Notification('error', 'Invalid Sonarr token'); let headers = { 'Accept': 'application/json', @@ -739,189 +931,463 @@ function getSonarr(options, api = "profile") { .then(response => response.json()) .catch(error => { return new Notification('error', 'Sonarr failed to connect with error:' + String(error)), - []; + []; }); } function performSonarrTest(QualityProfileID, StoragePath, refreshing = false) { let options = getOptionValues(), - teststatus = $$('#sonarr_test_status'), - path = $$('[data-option="sonarrURLRoot"]'), - storagepath = __sonarr_storagePath__, - quality = __sonarr_qualityProfile__, - url; + teststatus = $('#sonarr_test_status'), + path = $('[data-option="sonarrURLRoot"]'), + storagepath = __sonarr_storagePath__, + quality = __sonarr_qualityProfile__, + url, + enabled = $('#using-sonarr'); quality.innerHTML = ''; teststatus.textContent = '?'; - storagepath.textContent = ''; - options.sonarrURLRoot = url = path.value = options.sonarrURLRoot.replace(/^(\:\d+)/, 'localhost$1').replace(/^(?!^http(s)?:)/, 'http$1://').replace(/\/+$/, ''); - - let Get = () => { - getSonarr(options, 'profile').then(profiles => { - if(!profiles) return new Notification('error', 'Failed to get Sonarr configuration'); - - teststatus.textContent = '!'; - teststatus.classList = !!profiles.length; - - if(!profiles.length) - teststatus.title = 'Failed to communicate with Sonarr'; - - profiles.forEach(profile => { - let option = document.createElement('option'); - option.value = profile.id; - option.textContent = profile.name; - quality.appendChild(option); - }); - - // Because the was reset, the original value is lost. - if(StoragePath) - storagepath.value = StoragePath; - }); - }; - - if(refreshing) - Get(); - else if(url && url.length) - requestURLPermissions(url + '/*', allowed => - (allowed)? - Get(): - new Notification('error', 'The user refused permission to access Sonarr') - ); + storagepath.textContent = ''; + options.sonarrURLRoot = url = path.value = options.sonarrURLRoot.replace(/^(\:\d+)/, 'localhost$1').replace(/^(?!^http(s)?:)/, 'http$1://').replace(/\/+$/, ''); + + let Get = () => { + getSonarr(options, 'series').then(shows => { + shows.map(show => { + __caught.tvdb.push(show.tvdbId); + }); + }); + + getSonarr(options, 'profile').then(profiles => { + if(!profiles) return new Notification('error', 'Failed to get Sonarr configuration'); + + teststatus.textContent = '!'; + teststatus.classList = enabled.checked = !!profiles.length; + + if(!profiles.length) + return teststatus.title = 'Failed to communicate with Sonarr'; + enabled.parentElement.removeAttribute('disabled'); + + let qualities = []; + profiles.forEach(profile => { + let option = document.createElement('option'); + let { id, name } = profile; + + option.value = id; + option.textContent = name; + qualities.push({ id, name }); + quality.appendChild(option); + }); + + $('[data-option="sonarrQualities"i]').value = JSON.stringify(qualities); + + // Because the was reset, the original value is lost. + if(StoragePath) { + storagepath.value = StoragePath; + $('[data-option="__sonarrStoragePath"i]').value = StoragePaths.indexOf(StoragePath.replace(/\\/g, '/')) + 1; + } + }); + }; + + if(refreshing) + Get(); + else if(url && url.length) + requestURLPermissions(url + '/*', allowed => + (allowed)? + Get(): + new Notification('error', 'The user refused permission to access Sonarr') + ); +} + +function getMedusa(options, api = 'config') { + if(!options.medusaToken) + return new Notification('error', 'Invalid Medusa token'); + + let headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'X-Api-Key': options.medusaToken + }; + + if(options.medusaBasicAuthUsername) + headers.Authorization = `Basic ${ btoa(`${ options.medusaBasicAuthUsername }:${ options.medusaBasicAuthPassword }`) }`; + + return fetch(`${ options.medusaURLRoot }/api/v2/${ api }`, { headers }) + .then(response => response.json()) + .catch(error => { + return new Notification('error', 'Medusa failed to connect with error:' + String(error)), + []; + }); +} + +function performMedusaTest(QualityProfileID, StoragePath, refreshing = false) { + let options = getOptionValues(), + teststatus = $('#medusa_test_status'), + path = $('[data-option="medusaURLRoot"]'), + storagepath = __medusa_storagePath__, + quality = __medusa_qualityProfile__, + url, + enabled = $('#using-medusa'); + + quality.innerHTML = ''; + teststatus.textContent = '?'; + storagepath.textContent = ''; + options.medusaURLRoot = url = path.value = options.medusaURLRoot.replace(/^(\:\d+)/, 'localhost$1').replace(/^(?!^http(s)?:)/, 'http$1://').replace(/\/+$/, ''); + + let Get = () => { + getMedusa(options, 'series').then(shows => { + shows.map(show => { + __caught.imdb.push(show.id.imdb) + __caught.tvdb.push(show.id.tvdb); + }); + }); + + getMedusa(options, 'config').then(configuration => { + if(!configuration) return new Notification('error', 'Failed to get Medusa configuration'); + + let { qualities } = configuration.consts, + profileType = $('[data-option="medusaQualityProfileType"i]').selectedIndex, + profiles = (profileType == 0? 'presets': profileType == 1? 'values': 'anySets'); + + profiles = qualities[profiles]; + + teststatus.textContent = '!'; + teststatus.classList = enabled.checked = !!profiles.length; + + if(!profiles.length) + return teststatus.title = 'Failed to communicate with Medusa'; + enabled.parentElement.removeAttribute('disabled'); + + profiles.forEach(profile => { + let option = document.createElement('option'); + let { value, name } = profile; + + option.value = value; + option.textContent = name; + quality.appendChild(option); + }); + + $('[data-option="medusaQualities"i]').value = JSON.stringify(profiles); + + // Because the was reset, the original value is lost. + if(StoragePath) { + $('[data-option="__medusaStoragePath"i]').value = StoragePath; + storagepath.selectedIndex = StoragePaths.indexOf(StoragePath.replace(/\\/g, '/').replace(/\/+$/, '')); + } + }); + }; + + if(refreshing) + Get(); + else if(url && url.length) + requestURLPermissions(url + '/*', allowed => + (allowed)? + Get(): + new Notification('error', 'The user refused permission to access Medusa') + ); +} + +function getSickBeard(options, api = 'sb') { + if(!options.sickBeardToken) + return new Notification('error', 'Invalid Sick Beard token'); + + let headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'X-Api-Key': options.sickBeardToken, // not really used, but just in case... + }; + + if(options.sickBeardBasicAuthUsername) + headers.Authorization = `Basic ${ btoa(`${ options.sickBeardBasicAuthUsername }:${ options.sickBeardBasicAuthPassword }`) }`; + + return fetch(`${ options.sickBeardURLRoot }/api/${ options.sickBeardToken }/?cmd=${ api }`, { headers }) + .then(response => response.json()) + .catch(error => { + return new Notification('error', 'Sick Beard failed to connect with error:' + String(error)), + []; + }); +} + +function performSickBeardTest(QualityProfileID, StoragePath, refreshing = false) { + let options = getOptionValues(), + teststatus = $('#sickBeard_test_status'), + path = $('[data-option="sickBeardURLRoot"]'), + storagepath = __sickBeard_storagePath__, + quality = __sickBeard_qualityProfile__, + url, + enabled = $('#using-sickBeard'); + + quality.innerHTML = ''; + teststatus.textContent = '?'; + storagepath.textContent = ''; + options.sickBeardURLRoot = url = path.value = options.sickBeardURLRoot.replace(/^(\:\d+)/, 'localhost$1').replace(/^(?!^http(s)?:)/, 'http$1://').replace(/\/+$/, ''); + + let Get = () => { + getSickBeard(options, 'shows').then(shows => { + let _shows = shows.data; + + shows = []; + + for(let _show in _shows) + shows.push(_shows[_show]); + + shows.map(show => { + __caught.tvdb.push(show.tvdbid); + }); + }); + + getSickBeard(options, 'sb.getdefaults').then(configuration => { + if(!configuration) return new Notification('error', 'Failed to get Sick Beard configuration'); + + let qualities = configuration.data, + profileType = $('[data-option="sickBeardQualityProfileType"i]').selectedIndex, + profiles = (profileType == 0? 'initial': 'archive'); + + profiles = qualities[profiles]; + + teststatus.textContent = '!'; + teststatus.classList = enabled.checked = !!profiles.length; + + if(!profiles.length) + return teststatus.title = 'Failed to communicate with Sick Beard'; + enabled.parentElement.removeAttribute('disabled'); + + profiles = profiles.map((profile, index, array) => { + let option = document.createElement('option'); + let name = profile; + + option.value = option.textContent = name; + quality.appendChild(option); + + return { id: name, name }; + }); + + $('[data-option="sickBeardQualities"i]').value = JSON.stringify(profiles); + + // Because the was reset, the original value is lost. + if(StoragePath) { + $('[data-option="__sickBeardStoragePath"i]').value = + storagepath.selectedIndex = StoragePaths.indexOf(StoragePath.replace(/\\/g, '/').replace(/\/+$/, '')); + } + }); + }; + + if(refreshing) + Get(); + else if(url && url.length) + requestURLPermissions(url + '/*', allowed => + (allowed)? + Get(): + new Notification('error', 'The user refused permission to access Sick Beard') + ); +} + +function enableCouchPotato() { + $('#use-couchpotato').parentElement.removeAttribute('disabled'); } function HandleProxySettings(data) { - return { - enabled: data.UseProxy, - url: data.ProxyURL, - headers: data.ProxyHeaders, - }; + return { + enabled: data.UseProxy, + url: data.ProxyURL, + headers: data.ProxyHeaders, + }; } function saveOptions() { - ServerID = __servers__.options[__servers__.selectedIndex].value; + ServerID = [...__servers__.selectedOptions][0]; - if(!ServerID) { - let withoutplex = confirm('Continue without a Plex server?'); + if(!ServerID || !ServerID.value) { + let withoutplex = confirm('Continue without a Plex server?'); - if(withoutplex) - return saveOptionsWithoutPlex(); + if(withoutplex) + return saveOptionsWithoutPlex(); else - return new Notification('error', 'Select a server!'); - } + return new Notification('error', 'Select a server!'); + } + ServerID = ServerID.value; let server = PlexServers.find(ID => ID.clientIdentifier === ServerID); - // This should never happen, but can be useful for debugging. + // This should never happen, but can be useful for debugging. if(!server) return new Notification('error', `Could not find Plex server ${ ServerID }`), - null; + null; terminal.log('Selected server information:', server); // Important detail: we get the token from the selected server, NOT the token the user has entered before. let serverToken = server.accessToken, - serverConnections = getPlexConnections(server); - ClientID = server.clientIdentifier; + serverConnections = getPlexConnections(server); + ClientID = server.clientIdentifier; if(!serverConnections.length) return new Notification('error', 'Could not locate Plex server URL'), - null; + null; terminal.log('Plex Server connections:', serverConnections); // With a "user token" you can access multiple servers. A "normal" token is just for one server. let options = getOptionValues(), - endingSlash = ($0, $1, $$, $_) => ($1 + (/\\/.test($_)? '\\': '/')); - - options.DO_NOT_USE = false; - - let r, R = 'Radarr', - s, S = 'Sonarr', - w, W = 'Watcher', - c, C = 'CouchPotato', - o, O = 'Ombi'; - - let who = () => (r? R: s? S: w? W: c? C: o? O: 'manager'); - - // Instead of having the user be so wordy, complete the URL ourselves here - if((r = !options.radarrURLRoot && options.radarrToken) || (s = !options.sonarrURLRoot && options.sonarrToken) || (w = !options.watcherURLRoot && options.watcherToken) || (o = !options.ombiURLRoot && options.ombiToken)) { - return new Notification('error', `Please enter a valid URL for ${ who() }`), - null; - } if((options.radarrURLRoot && !options.radarrStoragePath) && (options.sonarrURLRoot && !options.sonarrStoragePath)) { - return new Notification('error', `Please enter a valid storage path for ${ who() }`), - null; - } if(options.watcherURLRoot && !options.watcherQualityProfileId) { - return new Notification('error', 'Select a quality profile for Watcher'), - null; - } if(options.radarrURLRoot && !options.radarrQualityProfileId) { - return new Notification('error', 'Select a quality profile for Radarr'), - null; - } if(options.sonarrURLRoot && !options.sonarrQualityProfileId) { - return new Notification('error', 'Select a quality profile for Sonarr'), - null; - } if(!ClientID) { - ClientID = window.crypto.getRandomValues(new Uint32Array(5)) - .join('-'); - storage.set({ ClientID }); - } - - options.plexURL = options.plexURLRoot = (options.plexURL || "https://app.plex.tv/") - .replace(/^(\:\d+)/, 'localhost$1') - .replace(/([^\\\/])$/, endingSlash) - .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); - - options.ombiURLRoot = (options.ombiURLRoot || "") - .replace(/([^\\\/])$/, endingSlash) - .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); - - options.watcherURLRoot = (options.watcherURLRoot || "") - .replace(/([^\\\/])$/, endingSlash) - .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); - - options.radarrURLRoot = (options.radarrURLRoot || "") - .replace(/([^\\\/])$/, endingSlash) - .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); - - options.sonarrURLRoot = (options.sonarrURLRoot || "") - .replace(/([^\\\/])$/, endingSlash) - .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); - - options.radarrStoragePath = options.radarrStoragePath - .replace(/([^\\\/])$/, endingSlash); - - options.sonarrStoragePath = options.sonarrStoragePath - .replace(/([^\\\/])$/, endingSlash); - - for(let index = 0, array = 'plex ombi watcher radarr sonarr couchpotato'.split(' '), item = save('URLs', array); index < array.length; index++) - save(`${ item = array[index] }.url`, options[`${ item }URLRoot`]); + endingSlash = ($0, $1, $$, $_) => ($1 + (/\\/.test($_)? '\\': '/')); + + options.IGNORE_PLEX = false; + + let r, R = 'Radarr', + s, S = 'Sonarr', + w, W = 'Watcher', + c, C = 'CouchPotato', + o, O = 'Ombi', + m, M = 'Medusa', + i, I = 'Sick Beard'; + + let who = () => (r? R: s? S: w? W: c? C: o? O: m? M: i? I: 'manager'); + + // Instead of having the user be so wordy, complete the URL ourselves here + if((r = !options.radarrURLRoot && options.radarrToken) || (s = !options.sonarrURLRoot && options.sonarrToken) || (w = !options.watcherURLRoot && options.watcherToken) || (o = !options.ombiURLRoot && options.ombiToken) || (m = !options.medusaURLRoot && options.medusaToken) || (i = !options.sickBeardURLRoot && options.sickBeardToken)) { + return new Notification('error', `Please enter a valid URL for ${ who() }`), + null; + } if((options.radarrURLRoot && !options.radarrStoragePath) && (options.sonarrURLRoot && !options.sonarrStoragePath) && (options.medusaURLRoot && !options.medusaStoragePath) && (options.sickBeardURLRoot && !options.sickBeardStoragePath)) { + return new Notification('error', `Please enter a valid storage path for ${ who() }`), + null; + } if(options.watcherURLRoot && !options.watcherQualityProfileId) { + return new Notification('error', 'Select a quality profile for Watcher'), + null; + } if(options.radarrURLRoot && !options.radarrQualityProfileId) { + return new Notification('error', 'Select a quality profile for Radarr'), + null; + } if(options.sonarrURLRoot && !options.sonarrQualityProfileId) { + return new Notification('error', 'Select a quality profile for Sonarr'), + null; + } if(options.medusaURLRoot && !options.medusaQualityProfileId) { + return new Notification('error', 'Select a quality profile for Medusa'), + null; + } if(options.sickBeardURLRoot && !options.sickBeardQualityProfileId) { + return new Notification('error', 'Select a quality profile for Sick Beard'), + null; + } if(!ClientID) { + ClientID = window.crypto.getRandomValues(new Uint32Array(5)) + .join('-'); + } + storage.set({ ClientID }); + + options.plexURL = options.plexURLRoot = (options.plexURL || "https://app.plex.tv/") + .replace(/^(\:\d+)/, 'localhost$1') + .replace(/([^\\\/])$/, endingSlash) + .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); + + options.ombiURLRoot = (options.ombiURLRoot || "") + .replace(/([^\\\/])$/, endingSlash) + .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); + + options.medusaURLRoot = (options.medusaURLRoot || "") + .replace(/([^\\\/])$/, endingSlash) + .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); + + options.watcherURLRoot = (options.watcherURLRoot || "") + .replace(/([^\\\/])$/, endingSlash) + .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); + + options.radarrURLRoot = (options.radarrURLRoot || "") + .replace(/([^\\\/])$/, endingSlash) + .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); + + options.sonarrURLRoot = (options.sonarrURLRoot || "") + .replace(/([^\\\/])$/, endingSlash) + .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); + + options.sickBeardURLRoot = (options.sickBeardURLRoot || "") + .replace(/([^\\\/])$/, endingSlash) + .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); + + options.radarrStoragePath = options.radarrStoragePath + .replace(/([^\\\/])$/, endingSlash); + + options.sonarrStoragePath = options.sonarrStoragePath + .replace(/([^\\\/])$/, endingSlash); + + options.medusaStoragePath = options.medusaStoragePath + .replace(/([^\\\/])$/, endingSlash); + + options.sickBeardStoragePath = options.sickBeardStoragePath + .replace(/([^\\\/])$/, endingSlash); + + // icons for the popup page + for(let index = 0, array = 'plex ombi medusa watcher radarr sonarr couchpotato sickBeard'.split(' '), item = save('URLs', array); index < array.length; index++) + save(`${ item = array[index] }.url`, options[`${ item }URLRoot`]); // Dynamically asking permissions requestURLPermissions(options.couchpotatoURLRoot); requestURLPermissions(options.watcherURLRoot); requestURLPermissions(options.radarrURLRoot); requestURLPermissions(options.sonarrURLRoot); + requestURLPermissions(options.medusaURLRoot); requestURLPermissions(options.ombiURLRoot); + requestURLPermissions(options.sickBeardURLRoot); - // Handle the proxy settings - options.proxy = HandleProxySettings(options); + // Handle the proxy settings + options.proxy = HandleProxySettings(options); function OptionsSavedMessage() { // Update status to let the user know the options were saved - new Notification('update', 'Saved', 3000); + new Notification('update', 'Saved', 1500); } - new Notification('update', 'Saving...', 3000); + new Notification('update', 'Saving...', 1500); let data = { ...options, @@ -939,91 +1405,115 @@ function saveOptions() { new Notification('error', 'Error with saving: ' + chrome.runtime.lastError.message); storage.set(data, OptionsSavedMessage); } else { - terminal.log('Saved Options: ' + JSON.stringify(options)); + terminal.log('Saved Options: ', options); OptionsSavedMessage(); } }); } function saveOptionsWithoutPlex() { - // See #4 + // See #4 let options = getOptionValues(), - endingSlash = ($0, $1, $$, $_) => ($1 + (/\\/.test($_)? '\\': '/')); - - options.DO_NOT_USE = true; - - let r, R = 'Radarr', - s, S = 'Sonarr', - w, W = 'Watcher', - c, C = 'CouchPotato', - o, O = 'Ombi'; - - let who = () => (r? R: s? S: w? W: c? C: o? O: 'manager'); - - // Instead of having the user be so wordy, complete the URL ourselves here - if((r = !options.radarrURLRoot && options.radarrToken) || (s = !options.sonarrURLRoot && options.sonarrToken) || (w = !options.watcherURLRoot && options.watcherToken) || (o = !options.ombiURLRoot && options.ombiToken)) { - return new Notification('error', `Please enter a valid URL for ${ who() }`), - null; - } if((options.radarrURLRoot && !options.radarrStoragePath) && (options.sonarrURLRoot && !options.sonarrStoragePath)) { - return new Notification('error', `Please enter a valid storage path for ${ who() }`), - null; - } if(options.watcherURLRoot && !options.watcherQualityProfileId) { - return new Notification('error', 'Select a quality profile for Watcher'), - null; - } if(options.radarrURLRoot && !options.radarrQualityProfileId) { - return new Notification('error', 'Select a quality profile for Radarr'), - null; - } if(options.sonarrURLRoot && !options.sonarrQualityProfileId) { - return new Notification('error', 'Select a quality profile for Sonarr'), - null; - } if(!ClientID) { - ClientID = 'web-to-plex:client'; - storage.set({ ClientID }); - } - - // Still need to set this - options.plexURL = options.plexURLRoot = "https://ephellon.github.io/web.to.plex/no.server/"; - - options.ombiURLRoot = (options.ombiURLRoot || "") - .replace(/([^\\\/])$/, endingSlash) - .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); - - options.watcherURLRoot = (options.watcherURLRoot || "") - .replace(/([^\\\/])$/, endingSlash) - .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); - - options.radarrURLRoot = (options.radarrURLRoot || "") - .replace(/([^\\\/])$/, endingSlash) - .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); - - options.sonarrURLRoot = (options.sonarrURLRoot || "") - .replace(/([^\\\/])$/, endingSlash) - .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); - - options.radarrStoragePath = options.radarrStoragePath - .replace(/([^\\\/])$/, endingSlash); - - options.sonarrStoragePath = options.sonarrStoragePath - .replace(/([^\\\/])$/, endingSlash); - - for(let index = 0, array = 'ombi watcher radarr sonarr couchpotato'.split(' '), item = save('URLs', array); index < array.length; index++) - save(`${ item = array[index] }.url`, options[`${ item }URLRoot`]); + endingSlash = ($0, $1, $$, $_) => ($1 + (/\\/.test($_)? '\\': '/')); + + options.IGNORE_PLEX = true; + + let r, R = 'Radarr', + s, S = 'Sonarr', + w, W = 'Watcher', + c, C = 'CouchPotato', + o, O = 'Ombi', + m, M = 'Medusa', + i, I = 'Sick Beard'; + + let who = () => (r? R: s? S: w? W: c? C: o? O: m? M: 'manager'); + + // Instead of having the user be so wordy, complete the URL ourselves here + if((r = !options.radarrURLRoot && options.radarrToken) || (s = !options.sonarrURLRoot && options.sonarrToken) || (w = !options.watcherURLRoot && options.watcherToken) || (o = !options.ombiURLRoot && options.ombiToken) || (m = !options.medusaURLRoot && options.medusaToken) || (i = !options.sickBeardURLRoot && options.sickBeardToken)) { + return new Notification('error', `Please enter a valid URL for ${ who() }`), + null; + } if((options.radarrURLRoot && !options.radarrStoragePath) && (options.sonarrURLRoot && !options.sonarrStoragePath) && (options.medusaURLRoot && !options.medusaStoragePath) && (options.sickBeardURLRoot && !options.sickBeardStoragePath)) { + return new Notification('error', `Please enter a valid storage path for ${ who() }`), + null; + } if(options.watcherURLRoot && !options.watcherQualityProfileId) { + return new Notification('error', 'Select a quality profile for Watcher'), + null; + } if(options.radarrURLRoot && !options.radarrQualityProfileId) { + return new Notification('error', 'Select a quality profile for Radarr'), + null; + } if(options.sonarrURLRoot && !options.sonarrQualityProfileId) { + return new Notification('error', 'Select a quality profile for Sonarr'), + null; + } if(options.medusaURLRoot && !options.medusaQualityProfileId) { + return new Notification('error', 'Select a quality profile for Medusa'), + null; + } if(options.sickBeardURLRoot && !options.sickBeardQualityProfileId) { + return new Notification('error', 'Select a quality profile for Sick Beard'), + null; + } if(!ClientID) { + ClientID = 'web-to-plex:client'; + storage.set({ ClientID }); + } + + // Still need to set this + options.plexURL = options.plexURLRoot = "https://ephellon.github.io/web.to.plex/no.server/"; + + options.ombiURLRoot = (options.ombiURLRoot || "") + .replace(/([^\\\/])$/, endingSlash) + .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); + + options.medusaURLRoot = (options.medusaURLRoot || "") + .replace(/([^\\\/])$/, endingSlash) + .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); + + options.watcherURLRoot = (options.watcherURLRoot || "") + .replace(/([^\\\/])$/, endingSlash) + .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); + + options.radarrURLRoot = (options.radarrURLRoot || "") + .replace(/([^\\\/])$/, endingSlash) + .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); + + options.sonarrURLRoot = (options.sonarrURLRoot || "") + .replace(/([^\\\/])$/, endingSlash) + .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); + + options.sickBeardURLRoot = (options.sickBeardURLRoot || "") + .replace(/([^\\\/])$/, endingSlash) + .replace(/^(?!^http(s)?:\/\/)(.+)/, 'http$1://$2'); + + options.radarrStoragePath = options.radarrStoragePath + .replace(/([^\\\/])$/, endingSlash); + + options.sonarrStoragePath = options.sonarrStoragePath + .replace(/([^\\\/])$/, endingSlash); + + options.medusaStoragePath = options.medusaStoragePath + .replace(/([^\\\/])$/, endingSlash); + + options.sickBeardStoragePath = options.sickBeardStoragePath + .replace(/([^\\\/])$/, endingSlash); + + // icons for the popup page + for(let index = 0, array = 'ombi medusa watcher radarr sonarr couchpotato sickBeard'.split(' '), item = save('URLs', array); index < array.length; index++) + save(`${ item = array[index] }.url`, options[`${ item }URLRoot`]); // Dynamically asking permissions requestURLPermissions(options.couchpotatoURLRoot); requestURLPermissions(options.watcherURLRoot); requestURLPermissions(options.radarrURLRoot); requestURLPermissions(options.sonarrURLRoot); + requestURLPermissions(options.medusaURLRoot); requestURLPermissions(options.ombiURLRoot); - // Handle the proxy settings - options.proxy = HandleProxySettings(options); + // Handle the proxy settings + options.proxy = HandleProxySettings(options); function OptionsSavedMessage() { // Update status to let the user know the options were saved - new Notification('update', 'Saved', 3000); + new Notification('update', 'Saved', 1500); } - new Notification('update', 'Saving...', 3000); + new Notification('update', 'Saving...', 1500); let data = options; @@ -1032,207 +1522,392 @@ function saveOptionsWithoutPlex() { new Notification('error', 'Error with saving: ' + chrome.runtime.lastError.message); storage.set(data, OptionsSavedMessage); } else { - terminal.log('Saved Options: ' + JSON.stringify(options)); + terminal.log('Saved Options: ', options); OptionsSavedMessage(); } }); } function requestURLPermissions(url, callback) { - if(url && callback) - return callback(true); - else if(url) - return true; - else - return false; - - /* DEAD CODE - DEAD CODE - DEAD CODE - DEAD CODE - DEAD CODE */ - /* DEAD CODE - DEAD CODE - DEAD CODE - DEAD CODE - DEAD CODE */ - /* DEAD CODE - DEAD CODE - BANANA 🍌 - DEAD CODE - DEAD CODE */ - /* DEAD CODE - DEAD CODE - DEAD CODE - DEAD CODE - DEAD CODE */ - /* DEAD CODE - DEAD CODE - DEAD CODE - DEAD CODE - DEAD CODE */ - - /* Obsolete, but may be useful later? */ - if(!url || /^https?\:\/\/\*/i.test(url)) - return; - - // TODO: Firefox doesn't have support for the chrome.permissions API. - if(chrome.permissions) { - // When asking permissions the URL needs to have a trailing slash. - chrome.permissions.request({ origins: [`${ url }`] }, callback); - } + if(url && callback) + return callback(true); + else if(url) + return true; + else + return false; + + /* DEAD CODE - DEAD CODE - DEAD CODE - DEAD CODE - DEAD CODE */ + /* DEAD CODE - DEAD CODE - DEAD CODE - DEAD CODE - DEAD CODE */ + /* DEAD CODE - DEAD CODE - BANANA 🍌 - DEAD CODE - DEAD CODE */ + /* DEAD CODE - DEAD CODE - DEAD CODE - DEAD CODE - DEAD CODE */ + /* DEAD CODE - DEAD CODE - DEAD CODE - DEAD CODE - DEAD CODE */ + + /* Obsolete, but may be useful later? */ + if(!url || /^https?\:\/\/\*/i.test(url)) + return; + + // TODO: Firefox doesn't have support for the chrome.permissions API. + if(chrome.permissions) { + // When asking permissions the URL needs to have a trailing slash. + chrome.permissions.request({ origins: [`${ url }`] }, callback); + } } // Restores select box and checkbox state using the preferences // stored in chrome.storage.* function restoreOptions(OPTIONS) { function setOptions(items) { - if(!items) return; + if(!items) return; __options__.forEach(option => { - let el = $$(`[data-option="${ option }"]`); - - if(!el) return; - - if(el.type == 'checkbox') - el.setAttribute('checked', el.checked = (typeof items[option] == 'boolean'? items[option]: el.getAttribute('checked') == 'true')); - else - el.value = items[option] || ''; - - if(el.value !== '' && !el.disabled) { - if(el.type == 'checkbox') - el.setAttribute('save', el.checked == 'true'); - else if(el.type == 'range') - el.setAttribute('save', el.value), - el.oninput({ target: el }); - else if(/password$/i.test(option)) - el.setAttribute('type', el.type = 'password'); - else - el.placeholder = `Last save: ${ el.value }`, - el.title = `Double-click to restore value ("${ el.value }")`, - el.setAttribute('save', el.value), - el.ondblclick = event => el.value = el.getAttribute('save'); - } - }); + let el = $(`[data-option="${ option }"]`); + + if(!el) return; + + if(el.type == 'checkbox') + el.setAttribute('checked', el.checked = (typeof items[option] == 'boolean'? items[option]: el.getAttribute('checked') == 'true')); + else + el.value = items[option] || ''; + + if(el.value !== '' && !el.disabled) { + if(el.type == 'checkbox') + el.setAttribute('save', el.checked == 'true'); + else if(el.type == 'range') + el.setAttribute('save', el.value), + el.oninput({ target: el }); + else if(/password$/i.test(option)) + el.setAttribute('type', el.type = 'password'); + else + el.placeholder = `Last save: ${ el.value }`, + el.title = `Double-click to restore value ("${ el.value }")`, + el.setAttribute('save', el.value), + el.ondblclick = event => el.value = el.getAttribute('save'); + } + }); if(items.plexToken) performPlexTest(items.servers? items.servers[0].id: null); - if(items.watcherURLRoot) + if(items.watcherURLRoot) performWatcherTest(items.watcherQualityProfileId, true); - if(items.ombiURLRoot) - performOmbiTest(true); - if(items.radarrURLRoot) + if(items.ombiURLRoot) + performOmbiTest(true); + if(items.medusaURLRoot) + performMedusaTest(items.medusaQualityProfileId, items.medusaStoragePath, true); + if(items.radarrURLRoot) performRadarrTest(items.radarrQualityProfileId, items.radarrStoragePath, true); - if(items.sonarrURLRoot) + if(items.sonarrURLRoot) performSonarrTest(items.sonarrQualityProfileId, items.sonarrStoragePath, true); + if(items.sickBeardURLRoot) + performSickBeardTest(items.sickBeardQualityProfileId, items.sickBeardStoragePath, true); + if(items.couchpotatoURLRoot) + enableCouchPotato(); + + let __domains = (sites => { + let array = []; + + for(let site in sites) + array.push(site); + + return array; + })({ ...builtin_sites, ...plugin_sites }); + + $('[data-option="__domains"i]').value = __domains; } + if (OPTIONS && typeof OPTIONS == 'string') { + OPTIONS = JSON.parse(OPTIONS); + + setOptions(OPTIONS); + } else { + storage.get(null, items => { + // Sigh... This is a workaround for Firefox; newer versions have support for the `chrome.storage.sync` API, + // BUT, it will throw an error if you haven't enabled it... + if(chrome.runtime.lastError) + storage.get(null, setOptions); + else + setOptions(items); + }); + } - if (OPTIONS && typeof OPTIONS == 'string') { - OPTIONS = JSON.parse(OPTIONS); - - setOptions(OPTIONS); - } else { - storage.get(null, items => { - // Sigh... This is a workaround for Firefox; newer versions have support for the `chrome.storage.sync` API, - // BUT, it will throw an error if you haven't enabled it... - if(chrome.runtime.lastError) - storage.get(null, setOptions); - else - setOptions(items); - }); - } + setTimeout(() => { + $('.checkbox:not([disabled]) input', true) + .forEach((element, index, array) => { + let options = getOptionValues(), + using = element.getAttribute('checked'); + + element.checked = using === 'true'; + }) + }, 250); } // Helpers document.furnish = function furnish(name, attributes = {}, ...children) { - let u = v => v && v.length, R = RegExp; + let u = v => v && v.length, R = RegExp; + + if( !u(name) ) + throw TypeError(`TAGNAME cannot be ${ (name === '')? 'empty': name }`); + + let options = attributes.is === true? { is: true }: null; + + delete attributes.is; + + name = name.split(/([#\.][^#\.\[\]]+)/).filter( u ); + + if(name.length <= 1) + name = name[0].split(/^([^\[\]]+)(\[.+\])/).filter( u ); + + if(name.length > 1) + for(let n = name, i = 1, l = n.length, t, v; i < l; i++) + if((v = n[i].slice(1, n[i].length)) && (t = n[i][0]) == '#') + attributes.id = v; + else if(t == '.') + attributes.classList = [].slice.call(attributes.classList || []).concat(v); + else if(/\[(.+)\]/.test(n[i])) + R.$1.split('][').forEach(N => attributes[(N = N.split('=', 2))[0]] = N[1] || ''); + name = name[0]; + + let element = document.createElement(name, options); + + if(attributes.classList instanceof Array) + attributes.classList = attributes.classList.join(' '); + + Object.entries(attributes).forEach( + ([name, value]) => (/^(on|(?:inner|outer)(?:HTML|Text)|textContent|class(?:List|Name)$|value)/.test(name))? + element[name] = value: + element.setAttribute(name, value) + ); + + children + .filter( child => child !== undefined && child !== null ) + .forEach( + child => + child instanceof Element? + element.append(child): + child instanceof Node? + element.appendChild(child): + element.appendChild(document.createTextNode(child)) + ); - if( !u(name) ) - throw TypeError(`TAGNAME cannot be ${ (name === '')? 'empty': name }`); + return element; +}; - let options = attributes.is === true? { is: true }: null; +// Default sites and their links +let builtins = { + "Netflix": "https://netflix.com/", + "Verizon": "https://tv.verizon.com/", + "Trakt": "https://trakt.tv/", + "Shana Project": "https://shanaproject.com/", + "YouTube": "https://youtube.com/", + "Rotten Tomatoes": "https://rottentomatoes.com/", + "Fandango": "https://www.fandango.com/", + "Amazon": "https://www.amazon.com/Amazon-Video/s/browse/ref=web_to_plex?node=2858778011", + "IMDb": "https://imdb.com/", + "Couch Potato": "http://couchpotato.life/", + "VRV": "https://vrv.co/", + "TMDb": "https://themoviedb.org/", + "Letterboxd": "https://letterboxd.com/", + "Hulu": "https://hulu.com/", + "Flickmetrix": "https://flickmetrix.com/", + "TVDb": "https://thetvdb.com/", + "Metacritic": "https://www.metacritic.com/", + "ShowRSS": "https://showrss.info/", + "Vudu": "https://vudu.com/", + "Movieo": "https://movieo.me/", + "Vumoo": "https://vumoo.to/", + "TV Maze": "https://tvmaze.com/", + "Google Play": "https://play.google.com/store/movies", + "Google": "https://google.com/", + "iTunes": "https://itunes.apple.com/", + "JustWatch": "https://justwatch.com/", + "MovieMeter": "https://moviemeter.nl/", + "GoStream": "https://gostream.site/", + "Tubi": "https://tubitv.com/", + "Web to Plex": "https://ephellon.github.io/web.to.plex/", + "Allocine": "https://allocine.fr/", + "Plex": "https://app.plex.tv/", + + // Dont' forget to add to the __options__ array! +}, builtin_array = [], builtin_sites = {}, builtinElement = $('#builtin'); + +for(let builtin in builtins) + builtin_array.push(builtin); +builtin_array = builtin_array.sort((a,b) => { let [x, y] = [a, b].map(v => v.toLowerCase()); return x < y? -1: 1; }); + +for(let index = 0, length = builtin_array.length; builtinElement && index < length; index++) { + let builtin = builtins[builtin_array[index]]; + + if(builtin instanceof Array) { + for(let i = 0, l = builtin.length; i < l; i++) { + let title = builtin_array[index], + name = 'builtin_' + title.toLowerCase().replace(/\s+/g, ''), + url = new URL(builtin[i]), + js = name.replace(/^builtin_/i, ''), + o = url.origin, + r = url.host.replace(/^(ww\w+\.)/, ''); + + builtin_sites[r] = o; + + if(!i) + builtinElement.innerHTML += +` +

${ title }

+
+ + +
+
+ Run on ${ title } +
- delete attributes.is; +
+`; + } + } else { + let title = builtin_array[index], + name = 'builtin_' + title.toLowerCase().replace(/\s+/g, ''), + url = new URL(builtins[title]), + js = name.replace(/^builtin_/i, ''), + o = url.origin, + r = url.host.replace(/^(ww\w+\.)/, ''); - name = name.split(/([#\.][^#\.\[\]]+)/).filter( u ); + builtin_sites[r] = o; - if(name.length <= 1) - name = name[0].split(/^([^\[\]]+)(\[.+\])/).filter( u ); + builtinElement.innerHTML += +` +

${ title }

+
+ + +
+
+ Run on ${ title } +
- if(name.length > 1) - for(let n = name, i = 1, l = n.length, t, v; i < l; i++) - if((v = n[i].slice(1, n[i].length)) && (t = n[i][0]) == '#') - attributes.id = v; - else if(t == '.') - attributes.classList = [].slice.call(attributes.classList || []).concat(v); - else if(/\[(.+)\]/.test(n[i])) - R.$1.split('][').forEach(N => attributes[(N = N.split('=', 2))[0]] = N[1] || ''); - name = name[0]; +
+`; + } - let element = document.createElement(name, options); + // save(`permission:${ r }`, true); + // save(`script:${ r }`, js); + // save(`builtin:${ r }`, true); +} - if(attributes.classList instanceof Array) - attributes.classList = attributes.classList.join(' '); +save('builtin.sites', builtin_sites); - Object.entries(attributes).forEach( - ([name, value]) => (/^(on|(?:inner|outer)(?:HTML|Text)|textContent|class(?:List|Name)$|value)/.test(name))? - element[name] = value: - element.setAttribute(name, value) - ); +$('[id^="builtin_"]', true) + .forEach(element => element.addEventListener('click', event => { + let self = event.target, + bid = self.getAttribute('bid'), + js = self.getAttribute('js'); - children - .filter( child => child !== undefined && child !== null ) - .forEach( - child => - child instanceof Element? - element.append(child): - child instanceof Node? - element.appendChild(child): - element.appendChild(document.createTextNode(child)) - ); + if(self.checked) { + terminal.log(bid, builtin_sites[bid]); + requestURLPermissions(builtin_sites[bid].replace(/https?:\/\/(ww\w+\.)?/i, '*://*.').replace(/\/?$/, '/*'), granted => { + save(`permission:${ bid }`, granted); + save(`script:${ bid }`, granted? js: null); + }); + } else { + save(`permission:${ bid }`, false); + save(`script:${ bid }`, null); + } - return element; -}; + save(`builtin:${ bid }`, true); + }) +); // Plugins and their links let plugins = { - 'Toloka': 'https://toloka.to/', - 'Shana Project': 'https://www.shanaproject.com/', - 'My Anime List': 'https://myanimelist.net/', - 'My Shows': 'https://myshows.me/', + 'Indomovie': ['https://indomovietv.club/', 'https://indomovietv.org/', 'https://indomovietv.net/'], + 'Toloka': 'https://toloka.to/', + 'Shana Project': 'https://www.shanaproject.com/', + 'My Anime List': 'https://myanimelist.net/', + 'My Shows': 'https://myshows.me/', + 'Redbox': 'https://www.redbox.com/', + 'Kitsu': 'https://kitsu.io/', - // Dont' forget to add to the __options__ array! -}, array = [], sites = {}, pluginElement = $$('#plugins'); + // Dont' forget to add to the __options__ array! +}, plugin_array = [], plugin_sites = {}, pluginElement = $('#plugins'); for(let plugin in plugins) - array.push(plugin); -array = array.sort(); + plugin_array.push(plugin); +plugin_array = plugin_array.sort((a,b) => { let [x, y] = [a, b].map(v => v.toLowerCase()); return x < y? -1: 1; }); + +for(let index = 0, length = plugin_array.length; pluginElement && index < length; index++) { + let plugin = plugins[plugin_array[index]]; + + if(plugin instanceof Array) { + for(let i = 0, l = plugin.length; i < l; i++) { + let title = plugin_array[index], + name = 'plugin_' + title.toLowerCase().replace(/\s+/g, ''), + url = new URL(plugin[i]), + js = name.replace(/^plugin_/i, ''), + o = url.origin, + r = url.host.replace(/^(ww\w+\.)/, ''); + + plugin_sites[r] = o; + + if(!i) + pluginElement.innerHTML += +` +

${ title }

+
+ + +
+
+ Run on ${ title } +
-for(let index = 0, length = array.length; pluginElement && index < length; index++) { - let title = array[index], - name = 'plugin_' + title.toLowerCase().replace(/\s+/g, ''), - url = new URL(plugins[title]), - js = name.replace(/^plugin_/i, ''), - o = url.origin, - r = url.host.replace(/^(ww\w+\.)/, ''); +
+`; + } + } else { + let title = plugin_array[index], + name = 'plugin_' + title.toLowerCase().replace(/\s+/g, ''), + url = new URL(plugins[title]), + js = name.replace(/^plugin_/i, ''), + o = url.origin, + r = url.host.replace(/^(ww\w+\.)/, ''); - sites[r] = o; + plugin_sites[r] = o; - pluginElement.innerHTML += + pluginElement.innerHTML += `

${ title }

- - + +
- Allows the ${ title } plugin to run on ${ r } + Run on ${ title }

`; + } } -save('optional.sites', sites); - -$$('[id^="plugin_"]', true) - .forEach(element => element.addEventListener('click', event => { - let self = event.target, - pid = self.getAttribute('pid'), - js = self.getAttribute('js'); - - if(self.checked) { - terminal.log(pid, sites[pid]) - requestURLPermissions(sites[pid].replace(/https?:\/\/(ww\w+\.)?/i, '*://*.').replace(/\/?$/, '/*'), granted => { - save(`permission:${ pid }`, granted); - save(`script:${ pid }`, granted? js: null); - }); - } else { - save(`permission:${ pid }`, false); - save(`script:${ pid }`, null); - } - }) +save('optional.sites', plugin_sites); + +$('[id^="plugin_"]', true) + .forEach(element => element.addEventListener('click', event => { + let self = event.target, + pid = self.getAttribute('pid'), + js = self.getAttribute('js'); + + if(self.checked) { + terminal.log(pid, plugin_sites[pid]); + requestURLPermissions(plugin_sites[pid].replace(/https?:\/\/(ww\w+\.)?/i, '*://*.').replace(/\/?$/, '/*'), granted => { + save(`permission:${ pid }`, granted); + save(`script:${ pid }`, granted? js: null); + }); + } else { + save(`permission:${ pid }`, false); + save(`script:${ pid }`, null); + } + + save(`builtin:${ pid }`, false); + }) ); let empty = () => {}; @@ -1240,73 +1915,142 @@ let empty = () => {}; document.addEventListener('DOMContentLoaded', restoreOptions); __save__.addEventListener('click', saveOptions); -$$('#plex_test') +$('#plex_test') .addEventListener('click', event => { - let pt = $$('#plex_token').value, - pu = $$('#plex_username').value, - pp = $$('#plex_password').value, - ou = $$('#ombi_url').value, - oa = $$('#ombi_api').value; - - if(pt) - performPlexTest(ServerID); - else if(pu && pp) - performPlexLogin(); - else if(ou && oa) - performOmbiLogin(); - }); -$$('#watcher_test', true).forEach(element => element.addEventListener('click', event => performWatcherTest())); -$$('#radarr_test', true).forEach(element => element.addEventListener('click', event => performRadarrTest())); -$$('#sonarr_test', true).forEach(element => element.addEventListener('click', event => performSonarrTest())); -$$('#ombi_test', true).forEach(element => element.addEventListener('click', event => performOmbiTest())); + let pt = $('#plex_token').value, + pu = $('#plex_username').value, + pp = $('#plex_password').value, + ou = $('#ombi_url').value, + oa = $('#ombi_api').value; + + if(pt) + performPlexTest(ServerID); + else if(pu && pp) + performPlexLogin(); + else if(ou && oa) + performOmbiLogin(); + }); +$('#watcher_test', true).forEach(element => element.addEventListener('click', event => performWatcherTest())); +$('#radarr_test', true).forEach(element => element.addEventListener('click', event => performRadarrTest())); +$('#sonarr_test', true).forEach(element => element.addEventListener('click', event => performSonarrTest())); +$('#medusa_test', true).forEach(element => element.addEventListener('click', event => performMedusaTest())); +$('#ombi_test', true).forEach(element => element.addEventListener('click', event => performOmbiTest())); +$('#sickBeard_test', true).forEach(element => element.addEventListener('click', event => performSickBeardTest())); +$('#enable-couchpotato', true).forEach(element => element.addEventListener('click', event => enableCouchPotato())); /* INPUT | Get the JSON data */ -$$('#json_get').addEventListener('click', event => { - let data_container = $$('#json_data'), - data = atob((data_container.value || data_container.textContent).replace(/\s*\[.+\]\s*/, '')); +$('#json_get').addEventListener('click', event => { + let data_container = $('#json_data'), + data = atob((data_container.value || data_container.textContent).replace(/\s*\[.+\]\s*/, '')); - if(!data) return new Notification('warning', 'The data cannot be blank, null, or undefined'); + if(!data) return new Notification('warning', 'The data cannot be blank, null, or undefined'); - try { - restoreOptions(data); + try { + restoreOptions(data); - new Notification('update', 'Restored configuration data', 3000); - } catch(error) { - new Notification('error', `Error restoring configuration data: ${ error }`); - } + new Notification('update', 'Restored configuration data', 3000); + } catch(error) { + new Notification('error', `Error restoring configuration data: ${ error }`); + } }); /* OUTPUT | Set the JSON data */ -$$('#json_set').addEventListener('click', event => { - let data_container = $$('#json_data'), - data = getOptionValues(); +$('#json_set').addEventListener('click', event => { + let data_container = $('#json_data'), + data = getOptionValues(); - data_container.value = data_container.textContent = `[${ (new Date).toString().slice(0, 24) }]${ btoa(JSON.stringify(data)) }`; + data_container.value = data_container.textContent = `[${ (new Date).toString().slice(0, 24) }]${ btoa(JSON.stringify(data)) }`; - new Notification('info', 'Copy the configuration data somewhere safe, use it to restore your options'); + new Notification('info', 'Copy the configuration data somewhere safe, use it to restore your options'); }); /* Erase Cached Searches */ -$$('#erase_cache').addEventListener('click', event => { - let options = JSON.stringify(getOptionValues()); +$('#erase_cache').addEventListener('click', event => { + let options = JSON.stringify(getOptionValues()); + + new Notification('info', 'Clearing...', 3000); + storage.get(null, items => { + for(let item in items) + if(/^~\/cache\//i.test(item)) + storage.remove(item); + }); + + saveOptions(event); +}); - storage.clear(); +$('#version') + .innerHTML = `Version ${ manifest.version }`; - restoreOptions(options); +$('[type="range"]', true) + .forEach((element, index, array) => { + let sibling = element.nextElementSibling, + symbol = element.getAttribute('symbol') || ''; - new Notification('info', 'Clearing...', 3000); + sibling.value = element.value + symbol; - setTimeout(saveOptions, 1000); // requires at least 1s for proper functioning -}); + element.oninput = (event, self) => (self = event.target).nextElementSibling.value = self.value + (self.getAttribute('symbol') || ''); + }); + +$('.checkbox', true) + .forEach((element, index, array) => { + element.addEventListener('click', event => { + let self = event.target; + + while(!~[...self.classList].indexOf('checkbox') && self.parentElement && self.parentElement != self) + self = self.parentElement; -$$('#version') - .innerHTML = `Version ${ chrome.manifest.version }`; -$$('[type="range"]', true) - .forEach((element, index, array) => { - let sibling = element.nextElementSibling, - symbol = element.getAttribute('symbol') || ''; + if('disabled' in self.attributes) + return event.preventDefault(true); + /* Stop the event from further processing */ + }); + }); - sibling.value = element.value + symbol; +$('.test', true) + .forEach((element, index, array) => { + element.addEventListener('click', async event => { + event.preventDefault(true); - element.oninput = (event, self) => (self = event.target).nextElementSibling.value = self.value + (self.getAttribute('symbol') || ''); - }); + let self = event.target; + + await saveOptions(event); + + open(self.href, self.target); + }); + }); + +$('[id^="theme:"i]', true) + .forEach((element, index, array) => { + element.addEventListener('click', async event => { + let self = event.target, + R = RegExp; + + let [a, b] = self.getAttribute('theme').split(/\s*:\s*/).filter(v => v), + value = `${self.id.replace(/^theme:/i, '')}-${b}`; + + if(/^(checkbox)$/i.test(self.type) && (self.checked + '') == a) + __theme.push(value); + else if(/^(text|input|button|\B)$/i.test(self.type) && R(self.value + '', 'i').test(a)) + __theme.push(value); + else + __theme = __theme.filter(v => v != value); + }); + }); + +// CORS exception: SecurityError +// MUST be { window }, never { top } +let { hash } = window.location; + +if(hash.length > 1) + switch(hash = hash.slice(1, hash.length).toLowerCase()) { + case 'save': + setTimeout(async() => { + await saveOptions(); + + window.postMessage({ type: 'INITIALIZE' }); + }, 1000); + break; + + default: + terminal.log(`Unknown event "${ hash }"`); + break; + }; diff --git a/src/options/test/blank.html b/src/options/test/blank.html new file mode 100644 index 0000000..4d9c7fd --- /dev/null +++ b/src/options/test/blank.html @@ -0,0 +1,5 @@ + + + + + diff --git a/src/options/test/index.html b/src/options/test/index.html new file mode 100644 index 0000000..4110c7e --- /dev/null +++ b/src/options/test/index.html @@ -0,0 +1,225 @@ + + + + Web to Plex | Testing + + + + + + + + +
+
+ + + +
+ + +
+ + + + + + + + + + + + diff --git a/src/options/test/index.js b/src/options/test/index.js new file mode 100644 index 0000000..b811ce6 --- /dev/null +++ b/src/options/test/index.js @@ -0,0 +1,104 @@ +let $ = selector => document.querySelector(selector); + +function modify({ type, title, year, info }) { + let object = { title, year, ...info }; + + $('#example').setAttribute('type', type); + + $('#movie').removeAttribute('active'); + $('#tv-show').removeAttribute('active'); + + $(`#${ type }`).setAttribute('active', true); + + let element; + for(let key in object) + if(element = $(`#${ key }`)) + element.innerHTML = object[key] || ""; + + $('#body').setAttribute('style', `background-image: url("${ type }.poster.jpg")`); + $('#poster').setAttribute('src', `${ type }.poster.jpg`); + + let { imdb, tmdb, tvdb } = object, + ids = { imdb, tmdb, tvdb }; + + for(let id in ids) + $(`#${ id }`).setAttribute('href', ( + ids[id]? + id == 'imdb'? + `https://www.imdb.com/videoembed/${ object[id.toUpperCase()] }/`: + id == 'tmdb'? + `https://www.youtube.com/embed/${ object[id.toUpperCase()] }`: + `https://www.youtube.com/embed/${ object[id.toUpperCase()] }`: + 'blank.html' + )); +} + +function as(type) { + open('blank.html', 'frame'); + + return modify({ + "movie": { + 'type': "movie", + 'title': "Being John Malkovich", + 'year': 1999, + 'info': { + 'rating': "R", + 'runtime': "1:53", + 'genre': "Comedy, Drama, Fantasy", + 'release-date': "December 3, 1999 (USA)", + + 'imdb': "tt0120601", + 'IMDB': "vi3568894233", + 'tmdb': 492, + 'TMDB': "HdVvjvW_OEo", + 'tvdb': null, + 'TVDB': null, + }, + }, + + "tv-show": { + 'type': "tv-show", + 'title': "Love, Death & Robots", + 'year': 2019, + 'info': { + 'rating': "TV-MA", + 'runtime': "0:15", + 'genre': "Animation, Comedy, Fantasy, Horror, Science-Fiction", + 'release-date': "May 15, 2019 (USA)", + + 'imdb': "tt9561862", + 'IMDB': "vi1035648281", + 'tmdb': 86831, + 'TMDB': "wUFwunMKa4E", + 'tvdb': 357888, + 'TVDB': "wUFwunMKa4E", + }, + }, + }[type]); +} + +document.querySelectorAll('#movie, #tv-show').forEach(element => { + element.onmouseup = event => { + let self = event.target; + + as(self.id); + }; +}); + +document.querySelectorAll('[target="frame"]').forEach(element => { + let body = document.body, + frame = $('#frame'), + loading = $('#loading'); + + element.onmouseup = event => { + loading.setAttribute('loading', true); + loading.removeAttribute('style'); + } + + frame.onload = frame.onerror = event => { + loading.setAttribute('loading', false); + setTimeout(() => loading.setAttribute('style', 'display:none'), 500); + } +}); + +document.body.onload = event => /#(movie|tv-show)/i.test(location.hash)? as(`${ location.hash.replace('#', '') }`): as('movie'); diff --git a/src/options/test/loading.png b/src/options/test/loading.png new file mode 100644 index 0000000..749aa93 Binary files /dev/null and b/src/options/test/loading.png differ diff --git a/src/options/test/movie.poster.jpg b/src/options/test/movie.poster.jpg new file mode 100644 index 0000000..efdc64f Binary files /dev/null and b/src/options/test/movie.poster.jpg differ diff --git a/src/options/test/noise.png b/src/options/test/noise.png new file mode 100644 index 0000000..7eead13 Binary files /dev/null and b/src/options/test/noise.png differ diff --git a/src/options/test/tv-show.poster.jpg b/src/options/test/tv-show.poster.jpg new file mode 100644 index 0000000..cc96009 Binary files /dev/null and b/src/options/test/tv-show.poster.jpg differ diff --git a/src/options/xml.js b/src/options/xml.js index 0592601..0bff785 100644 --- a/src/options/xml.js +++ b/src/options/xml.js @@ -9,11 +9,11 @@ function _flatten(object) { function _parse(xml) { let data = {}, - isText = xml.nodeType === 3, - isElement = xml.nodeType === 1, - body = xml.textContent && xml.textContent.trim(), - hasChildren = xml.children && xml.children.length, - hasAttributes = xml.attributes && xml.attributes.length; + isText = xml.nodeType === 3, + isElement = xml.nodeType === 1, + body = xml.textContent && xml.textContent.trim(), + hasChildren = xml.children && xml.children.length, + hasAttributes = xml.attributes && xml.attributes.length; // if it's text just return it if (isText) { diff --git a/src/plugn.js b/src/plugn.js index 9841ee9..03d591e 100644 --- a/src/plugn.js +++ b/src/plugn.js @@ -1,129 +1,755 @@ -/* Plugn.js (Plugin) - Web to Plex */ -/* global config */ +/* plugn.js (Plugin) - Web to Plex */ +/* global chrome */ + +let PLUGN_DEVELOPER = false; + +let PLUGN_TERMINAL = + PLUGN_DEVELOPER? + console: + { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m }; + +let LAST, LAST_JS, LAST_INSTANCE, LAST_ID, LAST_TYPE, FOUND = {}; + +let PLUGN_STORAGE = chrome.storage.sync || chrome.storage.local; +let PLUGN_CONFIGURATION; + +let URLRegExp = ` + .replace(/^\\*\\:/,'\\\\w{3,}:') + // *:// + .replace(/\\*\\./g,'(?:[^\\\\.]+\\\\.)?') + // *. + .replace(/\\.\\*/g,'(?:\\\\.[^\\\\/\\\\.]+)?') + // .* + .replace(/([\\/\\?\\&\\#])\\*/g,'$1[^$]*'),'i') + // /* OR ?* OR &* OR #* +`; + +function load(name, private) { + return JSON.parse((private && sessionStorage? sessionStorage: localStorage).getItem(btoa(name))); +} + +function save(name, data, private) { + return (private && sessionStorage? sessionStorage: localStorage).setItem(btoa(name), JSON.stringify(data)); +} + +async function Load(name = '') { + if(!name) + return /* invalid name */; + + name = '~/cache/' + (name.toLowerCase().replace(/\s+/g, '_')); + + return new Promise((resolve, reject) => { + function LOAD(DISK) { + let data = JSON.parse(DISK[name] || null); + + return resolve(data); + } + + PLUGN_STORAGE.get(null, DISK => { + if(chrome.runtime.lastError) + chrome.storage.local.get(null, LOAD); + else + LOAD(DISK); + }); + }); +} -let KILL_DEBUGGER = false; +async function Save(name = '', data) { + if(!name) + return /* invalid name */; -let logger = - KILL_DEBUGGER? - { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m }: - console; + name = '~/cache/' + (name.toLowerCase().replace(/\s+/g, '_')); + data = JSON.stringify(data); -function load(name) { - return JSON.parse(localStorage.getItem(btoa(name))); + await PLUGN_STORAGE.set({[name]: data}, () => data); + + return name; } -function save(name, data) { - return localStorage.setItem(btoa(name), JSON.stringify(data)); +function GetConsent(name, builtin) { + /* The configuration variable could fail to be initialized */ + if(!PLUGN_CONFIGURATION) + throw 'Configuration not found, exiting prematurely'; + + return PLUGN_CONFIGURATION[`${ (builtin? 'builtin': 'plugin') }_${ name }`]; } -function GetConsent(origin) { - return load('permission:' + origin); +async function GetAuthorization(name) { + let authorized = await Load(`has/${ name }`), + permissions = await Load(`get/${ name }`), + Ausername, Apassword, Atoken, + Aapi, Aserver, Aurl, Astorage, + Acache; + + if(!permissions) + return {}; + + function WriteOff(permission) { + if(/^(usernames?)$/i.test(permission)) + Ausername = true; + else if(/^(passwords?)$/i.test(permission)) + Apassword = true; + else if(/^(tokens?)$/i.test(permission)) + Atoken = true; + else if(/^(api)$/i.test(permission)) + Aapi = true; + else if(/^(servers?)$/i.test(permission)) + Aserver = true; + else if(/^(url(?:root)?)$/i.test(permission)) + Aurl = true; + else if(/^(storage)$/i.test(permission)) + Astorage = true; + else if(/^(cache)$/i.test(permission)) + Acache = true; + } + + if(permissions.constructor === Array) + for(let permission of permissions) + WriteOff(permission); + else if(permissions.constructor === Object) + for(let permission in permissions) + WriteOff(permission); + + return { authorized, Ausername, Apassword, Atoken, Aapi, Aserver, Aurl, Astorage, Acache }; } -let locationchangecallbacks = []; +// get the saved options +function getConfiguration() { + return new Promise((resolve, reject) => { + function handleConfiguration(options) { + if((!options.plexToken || !options.servers) && !options.IGNORE_PLEX) + return reject(new Error('Required options are missing')), + null; + + let server, o; + + if(!options.IGNORE_PLEX) { + // For now we support only one Plex server, but the options already + // allow multiple for easy migration in the future. + server = options.servers[0]; + o = { + server: { + ...server, + // Compatibility for users who have not updated their settings yet. + connections: server.connections || [{ uri: server.url }] + }, + ...options + }; + + options.plexURL = o.plexURL? + `${ o.plexURL }web#!/server/${ o.server.id }/`: + `https://app.plex.tv/web/app#!/server/${ o.server.id }/`; + } else { + o = options; + } + + if(o.couchpotatoBasicAuthUsername) + o.couchpotatoBasicAuth = { + username: o.couchpotatoBasicAuthUsername, + password: o.couchpotatoBasicAuthPassword + }; + + // TODO: stupid copy/pasta + if(o.watcherBasicAuthUsername) + o.watcherBasicAuth = { + username: o.watcherBasicAuthUsername, + password: o.watcherBasicAuthPassword + }; + + if(o.radarrBasicAuthUsername) + o.radarrBasicAuth = { + username: o.radarrBasicAuthUsername, + password: o.radarrBasicAuthPassword + }; + + if(o.sonarrBasicAuthUsername) + o.sonarrBasicAuth = { + username: o.sonarrBasicAuthUsername, + password: o.sonarrBasicAuthPassword + }; + + if(o.usingOmbi && o.ombiURLRoot && o.ombiToken) { + o.ombiURL = o.ombiURLRoot; + } else { + delete o.ombiURL; // prevent variable ghosting + } + + if(o.usingCouchPotato && o.couchpotatoURLRoot && o.couchpotatoToken) { + o.couchpotatoURL = `${ items.couchpotatoURLRoot }/api/${encodeURIComponent(o.couchpotatoToken)}`; + } else { + delete o.couchpotatoURL; // prevent variable ghosting + } + + if(o.usingWatcher && o.watcherURLRoot && o.watcherToken) { + o.watcherURL = o.watcherURLRoot; + } else { + delete o.watcherURL; // prevent variable ghosting + } + + if(o.usingRadarr && o.radarrURLRoot && o.radarrToken) { + o.radarrURL = o.radarrURLRoot; + } else { + delete o.radarrURL; // prevent variable ghosting + } + + if(o.usingSonarr && o.sonarrURLRoot && o.sonarrToken) { + o.sonarrURL = o.sonarrURLRoot; + } else { + delete o.sonarrURL; // prevent variable ghosting + } + + resolve(o); + } + + PLUGN_STORAGE.get(null, options => { + if(chrome.runtime.lastError) + chrome.storage.local.get(null, handleConfiguration); + else + handleConfiguration(options); + }); + }); +} -function watchlocationchange(subject) { - watchlocationchange[subject] = watchlocationchange[subject] || location[subject]; +// self explanatory, returns an object; sets the configuration variable +function parseConfiguration() { + return getConfiguration().then(options => { + PLUGN_CONFIGURATION = options; - if (watchlocationchange[subject] != location[subject]) { - watchlocationchange[subject] = location[subject]; + if((PLUGN_DEVELOPER = options.DeveloperMode) && !parseConfiguration.gotConfig) { + parseConfiguration.gotConfig = true; + PLUGN_TERMINAL = + PLUGN_DEVELOPER? + console: + { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m }; - for(let index = 0, length = locationchangecallbacks.length, callback; index < length; index++) { - callback = locationchangecallbacks[index]; + PLUGN_TERMINAL.warn(`PLUGN_DEVELOPER: ${PLUGN_DEVELOPER}`); + } - if(callback && typeof callback == 'function') - callback(new Event('locationchange', { bubbles: true })); - } - } + return options; + }, error => { throw error }); } -Object.defineProperty(window, 'onlocationchange', { - set: callback => locationchangecallbacks.push(callback) +chrome.storage.onChanged.addListener(async(changes, namespace) => { + await parseConfiguration(); }); -setInterval(() => watchlocationchange('pathname'), 1000); +(async() => { + await parseConfiguration(); +})(); function RandomName(length = 16, symbol = '') { - let values = []; + let values = []; - window.crypto.getRandomValues(new Uint32Array(length)).forEach((value, index, array) => values.push(value.toString(36))); + window.crypto.getRandomValues(new Uint32Array(length)).forEach((value, index, array) => values.push(value.toString(36))); - return values.join(symbol).replace(/^[^a-z]+/i, ''); + return values.join(symbol).replace(/^[^a-z]+/i, ''); }; -let running = [], instance = RandomName(), TAB; +function prepare(code, alias, type) { -let tabchange = tabs => { - let tab = tabs[0]; + let DATE = (new Date), + YEAR = DATE.getFullYear(), + MONT = DATE.getMonth(), + DAY = DATE.getDate(); - if(!tab) return; + return `let DATE = (new Date), + YEAR = ${YEAR}, + MONT = ${MONT}, + DAY = ${DAY}; - TAB = tab; +` + code +.replace(/\/\/+\s*"([^\"\n\f\r\v]+?)"\s*requires?\:?\s*(.+)([^]+)/i, ($0, $1, $2, $3, $$, $_) => ` +${ $3 } +;(async() => await Require("${ $2 }", "${ alias }", "${ $1 }"))(); +`) + ; +} - let id = tab.id, - url = tab.url, - can, org, ali, js; +let handle = async(results, tabID, instance, script, type) => { + let InstanceWarning = `[${ type.toUpperCase() }:${ script }] Instance failed to execute @${ tabID }#${ instance }`, + InstanceType = type; - if(!url || /^chrome/i.test(url) || (!!~running.indexOf(id) && !!~running.indexOf(instance))) - return /* - Stop if: - a) There isn't a url - b) The url is a chrome url - c) The tab AND instance are accounted for - */; + results = await results; - url = new URL(url); - org = url.origin; - ali = url.host.replace(/^(ww\w+\.|\w{2}\.)/i, ''); - can = GetConsent(ali); - js = load(`script:${ ali }`); + if((!results || !results[0] || !instance) && !FOUND[instance]) + try { + instance = RandomName(); + tabchange([ TAB ]); + return; + } catch(error) { + return PLUGN_TERMINAL.warn(InstanceWarning); + } - if(!can || !js) return; + let data = await results[0]; - let name = (KILL_DEBUGGER? instance: `top.${instance }`); // makes debugging easier + if(typeof data == 'number') { + if(handle.timeout) + return /* already running */; + if(data < 0) + return chrome.tabs.sendMessage(tabID, { data, instance, [InstanceType.toLowerCase()]: script, instance_type: InstanceType, type: 'NO_RENDER' }) + /* stop execution and timeouts/intervals */; - fetch(`https://ephellon.github.io/web.to.plex/plugins/${ js }.js`, { mode: 'cors' }) - .then(response => response.text()) - .then(code => { - chrome.tabs.executeScript(id, { file: 'helpers.js' }, () => { - // Sorry, but the instance needs to be callable multiple times - chrome.tabs.executeScript(id, { code: `${ name } = (${ name } || (()=>{'use strict';\n${ code }\n;return RegExp(plugin.url.replace(/\\|.*?(\\)|$)/g, '').replace(/^\\*\\:/, '\\\\w{3,}:').replace(/\\*\\./g, '([^\\\\.]+\\\\.)?'), 'i').test("${ url.href }")?plugin.init():console.warn("The domain '${ org }' ('${ url.href }') does not match the domain pattern '"+plugin.url+"'")\n})()); ${ name }` }, results => handle(results, id, instance, js)) - }) - }) - .then(() => running.push(id, instance)) - .catch(error => { throw error }); -}; + return handle.timeout = setTimeout(() => { let { request, sender, callback } = (processMessage.properties || {}); handle.timeout = null; processMessage(request, sender, callback) }, data); + } else if(typeof data == 'string') { + let R = RegExp; -window.onlocationchange = event => { - instance = RandomName(); - tabchange([TAB]); -}; + if(/^<([^<>]+)>$/.test(data)) + return PLUGN_TERMINAL.warn(`The instance requires the "${ R.$1 }" permission: ${ instance }`); + + data.replace(/^([^]+?)\s*\((\d{4})\):([\w\-]+)$/); + + let title = R.$1, + year = R.$2, + type = R.$3; + + data = { type, title, year }; + } -let handle = (results, tabID, instance, plugin) => { - let InstanceWarning = `Instance @${ tabID } [${ instance }] failed to execute`; + if(typeof data == 'number') + return setTimeout(() => { let { request, sender, callback } = (processMessage.properties || {}); processMessage(request, sender, callback) }, data); + if(typeof data != 'object') + return /* setTimeout */; - if(!results || !results[0] || !instance) - return logger.warn(InstanceWarning); + try { + if(data instanceof Array) { + data = data.filter(d => d); - let data = results[0]; + if(data.length > 1) { + chrome.tabs.sendMessage(tabID, { data, instance, [InstanceType.toLowerCase()]: script, instance_type: InstanceType, type: 'POPULATE' }); + return /* done */; + } - try { - chrome.tabs.executeScript(tabID, { file: 'utils.js' }, () => { - chrome.tabs.insertCSS(tabID, { file: 'sites/common.css' }); - chrome.tabs.sendMessage(tabID, { data, plugin, instance, type: 'POPULATE' }); - }); - } catch(error) { - throw new Error(InstanceWarning + ': ' + String(error)); - } + /* the array is too small to parse, set it as a single item */ + data = data[0]; + } + + let { type, title, year } = data; + + title = title + .replace(/[\u2010-\u2015]/g, '-') // fancy hyphen + .replace(/[\u201a\u275f]/g, ',') // fancy comma + .replace(/[\u2018\u2019\u201b\u275b\u275c]/g, "'") // fancy apostrophe + .replace(/[\u201c-\u201f\u275d\u275e]/g, '"'); // fancy quotation marks + year = +year; + + data = { ...data, type, title, year }; + + chrome.tabs.insertCSS(tabID, { file: 'sites/common.css' }); + chrome.tabs.sendMessage(tabID, { data, instance, [InstanceType.toLowerCase()]: script, instance_type: InstanceType, type: 'POPULATE' }); + } catch(error) { + throw new Error(InstanceWarning + ' - ' + String(error)); + } }; +let running = [], instance = RandomName(), TAB, cache = {}; + +/* Handle script/plugin events */ +let tabchange = async tabs => { + let tab = tabs[0]; + + if(!tab || FOUND[instance]) return; + + TAB = tab; + + let id = tab.id, + url = tab.url, + org, ali, js, + type, cached, + allowed; + + if( + !url + || /^(?:chrome|debugger|view-source)/i.test(url) + // || (!!~running.indexOf(id) && !!~running.indexOf(instance)) + ) + return /* + Stop if: + a) There isn't a url + b) The url is a chrome url + c) The tab AND instance are accounted for + */; + + url = new URL(url); + org = url.origin; + ali = url.host.replace(/^(ww\w+\.|\w{2}\.)/i, ''); + type = (load(`builtin:${ ali }`) + '') == 'true'? 'script': 'plugin'; + js = load(`${ type }:${ ali }`); + code = cache[ali]; + allowed = await GetConsent(ali, type == 'script'); + + if(!allowed || !js) return; + + let { authorized, ...A } = await GetAuthorization(js); + + if(code) { + chrome.tabs.executeScript(id, { file: 'helpers.js' }, () => { + // Sorry, but the instance needs to be callable multiple times + chrome.tabs.executeScript(id, { code }, results => handle(results, id, instance, js, type)); + }); + + return setTimeout(() => cache = {}, 1e6); + } + + let name = (!PLUGN_DEVELOPER? instance: `top.${ instance }`), // makes debugging easier + topmost = !/^top\./.test(name); + + let file = (PLUGN_DEVELOPER)? + (type === 'script')? + chrome.runtime.getURL(`cloud/${ js }.js`): + chrome.runtime.getURL(`cloud/plugin.${ js }.js`): + `https://ephellon.github.io/web.to.plex/${ type }s/${ js }.js`; + + await fetch(file, { mode: 'cors' }) + .then(response => response.text()) + .then(async code => { + await chrome.tabs.executeScript(id, { file: 'helpers.js' }, async() => { + // Sorry, but the instance needs to be callable multiple times + await chrome.tabs.executeScript(id, { code: + (LAST = cache[ali] = +`/* ${ type }* (${ (!PLUGN_DEVELOPER? 'on':'off') }line) - "${ url.href }" */ +${ topmost? 'var ': '' }${ name } = (${ name } || (${ name }$ = $ => { +'use strict'; + +/* Required permissions */ +if(${ allowed } === false) + return ''; +if(${ authorized } === false) + return ''; +${ + (() => { + let o = []; + + for(let a in A) + o.push( +`if(${ A[a] } === false) + return '<${ a.slice(1) }>'; +` + ); + + return o.join(''); + })() +} +/* Start Injected */ +${ prepare(code, js, type) } +/* End Injected */ + +let InjectedReadyState; + +top.addEventListener('popstate', ${ type }.init); +top.addEventListener('pushstate-changed', ${ type }.init); + +return (${ type }.RegExp = RegExp( + ${ type }.url +${ URLRegExp } +).test +(location.href)? +/* URL matches pattern */ + ${ type }.ready? + /* Injected file has the "ready" property */ + (InjectedReadyState = + ${ type }.ready.constructor.name == 'AsyncFunction'? + /* "ready" is an async function */ + ${ type }.ready(): + /* "ready" is a sync (normal) function */ + ${ type }.ready() + )? + /* Injected file is ready */ + ${ type }.init( InjectedReadyState ): + /* Injected file isn't ready */ + (${ type }.timeout || 1000): + /* Injected file doesn't have the "ready" property */ + ${ type }.init(): +/* URL doesn't match pattern */ +(console.warn("The domain '${ org }' (" + location.href + ") does not match the domain pattern '" + ${ type }.url + "' (" + ${ type }.RegExp + ")"), -1); +})(document.queryBy)); + +console.log('[${ name.replace(/^(top\.)?(\w{7}).*$/i, '$1$2') }]', ${ name }); + +top.onlocationchange = (event) => chrome.runtime.sendMessage({ type: '$INIT$', options: { ${ type }: '${ js }' } }); + +;${ name };` + ) }, results => handle(results, LAST_ID = id, LAST_INSTANCE = instance, LAST_JS = js, LAST_TYPE = type)) + }) + }) + .then(() => running.push(id, instance)) + .catch(error => { throw error }); +}; + +// listen for message event +let processMessage; + +chrome.runtime.onMessage.addListener(processMessage = async(request, sender, callback) => { + let { options } = request, + tab = TAB || {}, + { id, url, href } = tab, + org; + + processMessage.properties = { request, sender, callback }; + + if( + !url + || /^(?:chrome|debugger|view-source)/i.test(url) + // || (!!~running.indexOf(id) && !!~running.indexOf(instance)) + ) + return /* + Stop if: + a) There isn't a url + b) The url is a chrome url + c) The tab AND instance are accounted for + */; + + url = new URL(url); + org = url.origin; + + let name = (!PLUGN_DEVELOPER? instance: `top.${ instance }`), // makes debugging easier + topmost = !/^top\./.test(name); + + if(request && request.options) { + let { type } = request, + { plugin, script } = options, + _type = type.toLowerCase(), + allowed; + + type = type.toUpperCase(); + + let file = (PLUGN_DEVELOPER)? + (_type === 'script')? + chrome.runtime.getURL(`cloud/${ script }.js`): + chrome.runtime.getURL(`cloud/plugin.${ plugin }.js`): + `https://ephellon.github.io/web.to.plex/${ _type }s/${ options[_type] }.js`; + + let { authorized, ...A } = await GetAuthorization(options[_type]); + + switch(type) { + case 'PLUGIN': + allowed = await GetConsent(plugin, false); + + await fetch(file, { mode: 'cors' }) + .then(response => response.text()) + .then(async code => { + await chrome.tabs.executeScript(id, { file: 'helpers.js' }, async() => { + // Sorry, but the instance needs to be callable multiple times + await chrome.tabs.executeScript(id, { code: + (LAST = cache[plugin] = +`/* plugin (${ (!PLUGN_DEVELOPER? 'on':'off') }line) - "${ url.href }" */ +${ topmost? 'var ': '' }${ name } = (${ name } || (${ name }$ = $ => { +'use strict'; + +/* Required permissions */ +if(${ allowed } === false) + return ''; +if(${ authorized } === false) + return ''; +${ + (() => { + let o = []; + + for(let a in A) + o.push( +`if(${ A[a] } === false) + return '<${ a.slice(1) }>'; +` + ); + + return o.join(''); + })() +} +/* Start Injected (Plugin) */ +${ prepare(code, plugin, _type) } +/* End Injected */ + +let PluginReadyState; + +top.addEventListener('popstate', plugin.init); +top.addEventListener('pushstate-changed', plugin.init); + +return (plugin.RegExp = RegExp( + plugin.url +${ URLRegExp } +).test +(location.href)? +/* URL matches pattern */ + plugin.ready? + /* Plugin has the "ready" property */ + (PluginReadyState = + plugin.ready.constructor.name == 'AsyncFunction'? + /* "ready" is an async function */ + plugin.ready(): + /* "ready" is a sync (normal) function */ + plugin.ready() + )? + /* Plugin is ready */ + plugin.init( PluginReadyState ): + /* Script isn't ready */ + (plugin.timeout || 1000): + /* Plugin doesn't have the "ready" property */ + plugin.init(): +/* URL doesn't match pattern */ +(console.warn("The domain '${ org }' (" + location.href + ") does not match the domain pattern '" + plugin.url + "' (" + plugin.RegExp + ")"), -1); +})(document.queryBy)); + +console.log('[${ name.replace(/^(top\.)?(\w{7}).*$/i, '$1$2') }]', ${ name }); + +top.onlocationchange = (event) => chrome.runtime.sendMessage({ type: '$INIT$', options: { plugin: '${ plugin }' } }); + +;${ name };` +) }, results => handle(results, LAST_ID = id, LAST_INSTANCE = instance, LAST_JS = plugin, LAST_TYPE = type)) + }) + }) + .then(() => running.push(id, instance)) + .catch(error => { throw error }); + break; + + case 'SCRIPT': + allowed = await GetConsent(script, true); + + await fetch(file, { mode: 'cors' }) + .then(response => response.text()) + .then(async code => { + await chrome.tabs.executeScript(id, { file: 'helpers.js' }, async() => { + // Sorry, but the instance needs to be callable multiple times + await chrome.tabs.executeScript(id, { code: + (LAST = cache[script] = +`/* script (${ (!PLUGN_DEVELOPER? 'on':'off') }line) - "${ url.href }" */ +${ topmost? 'var ': '' }${ name } = (${ name } || (${ name }$ = $ => { +'use strict'; + +/* Required permissions */ +if(${ allowed } === false) + return ''; +if(${ authorized } === false) + return ''; +${ + (() => { + let o = []; + + for(let a in A) + o.push( +`if(${ A[a] } === false) + return '<${ a.slice(1) }>'; +` + ); + + return o.join(''); + })() +} +/* Start Injected (Script) */ +${ prepare(code, script, _type) } +/* End Injected */ + +let ScriptReadyState; + +top.addEventListener('popstate', script.init); +top.addEventListener('pushstate-changed', script.init); + +return (script.RegExp = RegExp( + script.url +${ URLRegExp } +).test +(location.href)? +/* URL matches pattern */ + script.ready? + /* Script has the "ready" property */ + (ScriptReadyState = + script.ready.constructor.name == 'AsyncFunction'? + /* "ready" is an async function */ + script.ready(): + /* "ready" is a sync (normal) function */ + script.ready() + )? + /* Script is ready */ + script.init( ScriptReadyState ): + /* Script isn't ready */ + (script.timeout || 1000): + /* Script doesn't have the "ready" property */ + script.init(): +/* URL doesn't match pattern */ +(console.warn("The domain '${ org }' (" + location.href + ") does not match the domain pattern '" + script.url + "' (" + script.RegExp + ")"), -1); +})(document.queryBy)); + +console.log('[${ name.replace(/^(top\.)?(\w{7}).*$/i, '$1$2') }]', ${ name }); + +top.onlocationchange = (event) => chrome.runtime.sendMessage({ type: '$INIT$', options: { script: '${ script }' } }); + +;${ name };` + ) }, results => handle(results, LAST_ID = id, LAST_INSTANCE = instance, LAST_JS = script, LAST_TYPE = type)) + }) + }) + .then(() => running.push(id, instance)) + .catch(error => { throw error }); + break; + + // Soft reset (button reset) + case '_INIT_': + chrome.tabs.executeScript(id, { code: LAST }, results => handle(results, LAST_ID, LAST_INSTANCE, LAST_JS, LAST_TYPE)); + break; + + // Hard reset (program reset) + case '$INIT$': + let t = type.toLowerCase(), + data = {}; + + chrome.tabs.sendMessage(tab.id, { data, instance, [t]: script, instance_type: t, type: 'INITIALIZE' }); + // chrome.tabs.getCurrent(tab => { + // instance = RandomName(); + // + // setTimeout(() => tabchange([ tab ]), 5000); + // }); + break; + + case 'FOUND': + FOUND[request.instance] = request.found; + break; + + case 'GRANT_PERMISSION': + await Save(`has/${ options[_type] }`, options.allowed); + await Save(`get/${ options[_type] }`, options.permissions); + break; + + case 'SEARCH_PLEX': + case 'VIEW_COUCHPOTATO': + case 'PUSH_COUCHPOTATO': + case 'PUSH_RADARR': + case 'PUSH_SONARR': + case 'PUSH_MEDUSA': + case 'PUSH_WATCHER': + case 'PUSH_OMBI': + case 'PUSH_SICKBEARD': + case 'OPEN_OPTIONS': + case 'SEARCH_FOR': + case 'SAVE_AS': + case 'DOWNLOAD_FILE': + /* Meant to be hnadled by background.js */ + return true; + break; + + default: + PLUGN_TERMINAL.warn(`Unable to find type "${ type }"`); + instance = RandomName(); + return false; + }; + } + + return true; +}); + // this doesn't actually work... -//chrome.tabs.onActiveChanged.addListener(tabchange); +// chrome.tabs.onActiveChanged.addListener(tabchange); // workaround for the above -setInterval(() => - chrome.tabs.query({ - active: true, - currentWindow: true, - }, tabchange) -, 1000); +chrome.tabs.onActivated.addListener(change => { + instance = RandomName(); + + chrome.tabs.get(change.tabId, tab => tabchange([ tab ])); +}); + +let refresh; + +chrome.tabs.onUpdated.addListener(refresh = (ID, change, tab) => { + instance = RandomName(); + + if(change.status == 'complete' && !tab.discarded) + tabchange([ tab ]); + else if(!tab.discarded) + setTimeout(() => refresh(ID, change, tab), 1000); +}); diff --git a/src/popup/index.css b/src/popup/index.css new file mode 100644 index 0000000..eca5674 --- /dev/null +++ b/src/popup/index.css @@ -0,0 +1,311 @@ +html, body { + height: 625px !important; + width: 625px !important; +} + +body { + background: url(../img/noise.png), #3f4245 !important; + color: #333 !important; + font-family: Open Sans Regular, Helvetica Neue, Helvetica, Arial, sans-serif, system; + font-size: 1.25em !important; + flex-grow: 1 !important; + padding: 1px !important; + overflow: hidden !important; + position: absolute !important; +} + +a { + text-decoration: none !important; + cursor: pointer !important; + margin: 0 !important; +} + +img { + vertical-align: middle !important; + padding: 0 1em !important; +} + +label { + color: #eee !important; + cursor: inherit !important; + font-weight: 400 !important; + display: inline-block !important; + margin-bottom: 5px; + display: block !important; + position: relative !important; +/* + height: 165px !important; + width: 170px !important; +*/ +} + +table, footer { + height: 35% !important; + width: 100% !important; + font-size: 1em !important; + overflow: auto !important; + color: #fff !important; + font-weight: 400 !important; + padding-bottom: 1em !important; +} + +tr, footer > * { + box-shadow: none !important; + height: calc(33% - 25px); + width: calc(100% - 25px); + padding: 0 !important; +} + +td, footer > * > * { + padding: 15px !important; + line-height: 1.33 !important; + border-radius: 3px; + cursor: pointer !important; + text-transform: uppercase; + border: 0; + background: rgba(255, 255, 255, 0.3); + margin-bottom: 0; + vertical-align: middle; + white-space: nowrap; + user-select: none; + height: calc(100% - 30px); + width: calc(100% - 30px); + transition: box-shadow 0.5s !important; + display: table-cell !important; +} + +#movieo:hover { + box-shadow: 0 10px 128px inset #5DBCD4; +} + +#imdb:hover { + box-shadow: 0 10px 128px inset #E0AB00; +} + +#trakt:hover { + box-shadow: 0 10px 128px inset #ED2224; +} + +#letterboxd:hover { + box-shadow: 0 10px 128px inset #66CC33; +} + +#flenix:hover { + box-shadow: 0 10px 128px inset #EC164F; +} + +#tv-maze:hover { + box-shadow: 0 10px 128px inset #6EC4BA; +} + +#tvdb:hover { + box-shadow: 0 10px 128px inset #1C7E3E; +} + +#tmdb:hover { + box-shadow: 0 10px 128px inset #01D277; +} + +#vrv:hover { + box-shadow: 0 10px 128px inset #FFDD00; +} + +#hulu:hover { + box-shadow: 0 10px 128px inset #66AA33; +} + +#netflix:hover { + box-shadow: 0 10px 128px inset #E50914; +} + +#google:hover { + box-shadow: 0 10px 128px inset #EEEEEE; +} + +#itunes:hover { + box-shadow: 0 10px 128px inset #EEEEEE; +} + +#metacritic:hover { + box-shadow: 0 10px 128px inset #001B36; +} + +#fandango:hover { + box-shadow: 0 10px 128px inset #FF7300; +} + +#amazon:hover { + box-shadow: 0 10px 128px inset #FF9900; +} + +#vudu:hover { + box-shadow: 0 10px 128px inset #027FC5; +} + +#verizon:hover { + box-shadow: 0 10px 128px inset #E10000; +} + +#couch-potato:hover { + box-shadow: 0 10px 128px inset #ECB501; +} + +#rotten-tomatoes:hover { + box-shadow: 0 10px 128px inset #FA3008; +} + +#showrss:hover { + box-shadow: 0 10px 128px inset #6A8592; +} + +#vumoo:hover { + box-shadow: 0 10px 128px inset #DD1B2F; +} + +#shana-project:hover { + box-shadow: 0 10px 128px inset #FF0000; +} + +#youtube:hover { + box-shadow: 0 10px 128px inset #FF0000; +} + +#flickmetrix:hover { + box-shadow: 0 10px 128px inset #7A314E; +} + +#justwatch:hover { + box-shadow: 0 10px 128px inset #0E202C; +} + +#moviemeter:hover { + box-shadow: 0 10px 128px inset #000000; +} + +#allocine:hover { + box-shadow: 0 10px 128px inset #222222; +} + +#gostream:hover { + box-shadow: 0 10px 128px inset #028CC9; +} + +#tubi:hover { + box-shadow: 0 10px 128px inset #26262D; +} + +#webtoplex:hover { + box-shadow: 0 10px 128px inset #CC7B19; +} + +#local-plex:hover { + box-shadow: 0 10px 128px inset #F9BD03; +} + +#local-watcher:hover { + box-shadow: 0 10px 128px inset #EA554E; +} + +#local-radarr:hover { + box-shadow: 0 10px 128px inset #FFC230; +} + +#local-sonarr:hover { + box-shadow: 0 10px 128px inset #36C6F4; +} + +#local-couchpotato:hover { + box-shadow: 0 10px 128px inset #D20000; +} + +#local-ombi:hover { + box-shadow: 0 10px 128px inset #E48F34; +} + +#local-medusa:hover { + box-shadow: 0 10px 128px inset #26B043; +} + +#local-sickBeard:hover { + box-shadow: 0 10px 128px inset #296737; +} + +[save-file]:after, [cost-cash-low]:after, [cost-cash-med]:after, [cost-cash-hig]:after { + content: "____"; + color: transparent; + float: right; + width: 3em; + height: 3em; + margin-top: -8.5em; + margin-right: -1em; +} + +[pop-ups] label:after { + content: " \1F92C"; + float: right; +} + +[local] label:after { + content: " \1F5A5"; + float: right; +} + +[is-slow] label:after { + content: " \23f3"; + float: right; +} + +[is-shy] label:after, [is-dead] label:after { + content: " \1f910"; + float: right; +} + +[save-file]:after { + background: url("../img/48.png") no-repeat center; +} + +[not-safe] label:after { + content: " \1F527"; + float: right; +} + +/* $1 - $10 */ +[cost-cash-low]:after { + background: url("../img/$48.png") no-repeat center; +} + +/* $11 - $30 */ +[cost-cash-med]:after { + background: url("../img/$$48.png") no-repeat center; +} + +/* $31+ */ +[cost-cash-hig]:after { + background: url("../img/$$$48.png") no-repeat center; +} + +[disabled], [disabled]:hover { +/* box-shadow: 0 10px 128px inset #000 !important;*/ + opacity: 0.5 !important; +} + +[disabled] label:after { + content: " \23f3" !important; + float: right; +} + +*::-webkit-scrollbar { + width: 10px; +} + +*::-webkit-scrollbar-thumb { + min-height: 50px; + background: rgba(255, 255, 255, 0.15); + border: 2px solid rgba(0, 0, 0, 0); + border-radius: 8px; + background-clip: padding-box; +} + +*::-webkit-scrollbar-track { + background: url(../img/noise.png) repeat, #3f4245 !important; +} diff --git a/src/popup/index.html b/src/popup/index.html index f8adb7b..ca9d0ae 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -1,284 +1,10 @@ - Web to Plex - - + Web to Plex + @@ -289,7 +15,7 @@ - - + - + + + - - - + + + - - - - - @@ -394,42 +120,48 @@ + + + + + - - - - + + + - - - - - + + + + + + + + + + +
+ Verizon @@ -310,7 +36,7 @@ + YouTube @@ -325,27 +51,33 @@
+ + Vumoo + + + fandango + Amazon
IMDb
CouchPotato @@ -358,33 +90,27 @@
The MovieDb
Letterboxd + Hulu - - Flickmetrix - - -
+ + Flickmetrix + + + + + JustWatch + + +
iTunes + showRSS
Vudu
Movieo - - GoStream - - -
TV Maze @@ -442,12 +174,48 @@ +
iTunes + + Allocine + + + + + MovieMeter + + +
+ + GoStream + + + + + Tubi + + + + + Web to Plex + + +
diff --git a/src/popup/index.js b/src/popup/index.js index ac75a63..1978f0a 100644 --- a/src/popup/index.js +++ b/src/popup/index.js @@ -1,88 +1,92 @@ function load(name) { - return JSON.parse(localStorage.getItem(btoa(name))); + return JSON.parse(localStorage.getItem(btoa(name))); } function save(name, data) { - return localStorage.setItem(btoa(name), JSON.stringify(data)); + return localStorage.setItem(btoa(name), JSON.stringify(data)); } let table = document.body.querySelector('table'), - array = load('URLs'); + array = load('URLs'); if(array && array.length) { - let strings = [], - compiled = [], - object = {}, - width = 3; + let strings = [], + compiled = [], + object = {}, + width = 3; - for(let count = 0, length = Math.ceil(array.length / width); count < length;) - for(let index = width * count++, name, url; index < count * width; index++) - object[name = array[index]] = (!/^(null|undefined)?$/.test( url = load(`${ name }.url`) || '' ))? - ` - - ${ name } - - - `: null; + for(let count = 0, length = Math.ceil(array.length / width); count < length;) + for(let index = width * count++, name, url; index < count * width; index++) + object[name = array[index]] = (!/^(null|undefined)?$/.test( url = load(`${ name }.url`) || '' ))? + ` + + ${ name } + + + `: null; - for(let index = 0, length = array.length, string; index < length; index++) - if(string = object[array[index]]) - compiled.push(string); + for(let index = 0, length = array.length, string; index < length; index++) + if(string = object[array[index]]) + compiled.push(string); - for(let index = 0, length = compiled.length, string = ''; index <= length; index++) { - if((index > 0 && index % 3 == 0) || index >= length) - strings.push(string), - string = ''; - if(index < length) - string += compiled[index]; - } + for(let index = 0, length = compiled.length, string = ''; index <= length; index++) { + if((index > 0 && index % 3 == 0) || index >= length) + strings.push(string), + string = ''; + if(index < length) + string += compiled[index]; + } - let html = ''; + let html = ''; - strings.map(string => - html += + strings.map(string => + html += ` - ${ string } - ` - ); + ${ string } + ` + ); - table.innerHTML = html + table.innerHTML; + table.innerHTML = html + table.innerHTML; } document.body.onload = function() { - let messages = { - "and": "{:{*}}", - "disabled": "Not yet implemented", - "is-shy": "Can only be accessed via: {*}", - "is-slow": "Resource intensive (loads slowly)", - "local": "Opens a link to ^{*}", - "pop-ups": "Contains annoying/intrusive ads and/or pop-ups", - "save-file": "Uses {*} before using your manager(s)", - // $0.99 one time; $0.99 - $9.99/mon - "cost-cash-low": "At least {*} (fair)", - // $9.99 one time; $9.99 - $29.99/mon - "cost-cash-med": "At least {*} (pricy)", - // $29.99 one time; $29.99 - $99.99/mon - "cost-cash-hig": "At least {*} (expensive)" - }, - parse = (string, attribute, element) => { - return string - .replace(/\{\$\}/g, element.title) - .replace(/\{\*\}/g, element.getAttribute(attribute)) - .replace(/\{\:([\w\- ]+)\}/g, ($0, $1, $$, $_) => - $1.split(' ').map($1 => parse(element.getAttribute($1), $1, element)) - ) - .replace(/\^([a-z])/gi, ($0, $1, $$, $_) => $1.toUpperCase()); - }, - selectors = []; + let messages = { + "and": "{:{*}}", + "disabled": "Not yet implemented", + "is-shy": "Can only be accessed via: {*}", + "is-slow": "Resource intensive (loads slowly)", + "is-dead": "Isn't meant to show the Web to Plex button", + "local": "Opens a link to ^{*}", + "not-safe": "Updated irregularly, may drop support", + "pop-ups": "Contains annoying/intrusive ads and/or pop-ups", + "save-file": "Uses {*} before using your manager(s)", + // $0.99 one time; $0.99 - $9.99/mon + "cost-cash-low": "At least {*} (fair)", + // $9.99 one time; $9.99 - $29.99/mon + "cost-cash-med": "At least {*} (pricy)", + // $29.99 one time; $29.99 - $99.99/mon + "cost-cash-hig": "At least {*} (expensive)" + }, + parse = (string, attribute, element) => { + return string + .replace(/\{\$\}/g, element.title) + .replace(/\{\*\}/g, element.getAttribute(attribute)) + .replace(/\{\:([\w\- ]+)\}/g, ($0, $1, $$, $_) => + $1.split(' ').map($1 => parse(element.getAttribute($1), $1, element)) + ) + .replace(/\^([a-z])/gi, ($0, $1, $$, $_) => $1.toUpperCase()); + }, + selectors = []; - for(let key in messages) - selectors.push(`[${ key }]`); + for(let key in messages) + selectors.push(`[${ key }]`); - let elements = document.querySelectorAll(selectors.join(',')); + let elements = document.querySelectorAll(selectors.join(',')); - for(let element, index = 0, length = elements.length; index < length; index++) - for(let attribute in messages) - if(attribute in (element = elements[index]).attributes) - element.title = `${ parse(messages[attribute], attribute, element) }. ${ element.title }`; + for(let element, index = 0, length = elements.length; index < length; index++) { + let number = 1; + for(let attribute in messages) + if(attribute in (element = elements[index]).attributes) + element.title += `\n${(number++)}) ${ parse(messages[attribute], attribute, element) }.`; + } } diff --git a/src/sites/__layout__.js b/src/sites/__layout__.js new file mode 100644 index 0000000..9831075 --- /dev/null +++ b/src/sites/__layout__.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: '< Page Alias >' }))(); diff --git a/src/sites/__test__.js b/src/sites/__test__.js new file mode 100644 index 0000000..68ee2d5 --- /dev/null +++ b/src/sites/__test__.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: '__test__' }))(); diff --git a/src/sites/flenix/index.css b/src/sites/allocine/index.css similarity index 100% rename from src/sites/flenix/index.css rename to src/sites/allocine/index.css diff --git a/src/sites/allocine/index.js b/src/sites/allocine/index.js new file mode 100644 index 0000000..850536c --- /dev/null +++ b/src/sites/allocine/index.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'allocine' }))(); diff --git a/src/sites/amazon/index.js b/src/sites/amazon/index.js index adc5d72..70796df 100644 --- a/src/sites/amazon/index.js +++ b/src/sites/amazon/index.js @@ -1,58 +1,2 @@ -/* global findPlexMedia, parseOptions, modifyPlexButton */ -function isMovie() { - return !isShow(); -} - -function isShow() { - return document.querySelector('[data-automation-id*="seasons"], [class*="seasons"], [class*="episodes"], [class*="series"]'); -} - -function isPageReady() { - return document.querySelector('[data-automation-id="imdb-rating-badge"], #most-recent-reviews-content > *:first-child'); -} - -async function init() { - if (isPageReady()) - await initPlexThingy(isShow()? 'tv': 'movie'); - else - // This almost never happens, but sometimes the page is too slow so we need to wait a bit. - setTimeout(init, 1000); -} - -async function initPlexThingy(type) { - let button = renderPlexButton(), - R = RegExp; - - if (!button) - return /* Fatal Error: Fail Silently */; - - let $title = document.querySelector('[data-automation-id="title"], #aiv-content-title, .dv-node-dp-title'), - $year = document.querySelector('[data-automation-id="release-year-badge"], .release-year'), - $image = document.querySelector('.av-bgimg__div, div[style*="sgp-catalog-images"]'); - - if (!$title) - return modifyPlexButton( - button, - 'error', - 'Could not extract title from Amazon' - ), - null; - - let title = $title.textContent.replace(/(?:\(.+?\)|(\d+)|\d+\s+seasons?\s+(\d+))\s*$/gi, '').trim(), - year = $year? $year.textContent.trim(): R.$1 || R.$2 || YEAR, - image = getComputedStyle($image).backgroundImage; - - let Db = await getIDs({ title, year, type }), - IMDbID = Db.imdb, - TMDbID = Db.tmdb, - TVDbID = Db.tvdb; - - title = Db.title; - year = Db.year; - - findPlexMedia({ type, title, year, image, button, IMDbID, TMDbID, TVDbID }); -} - -if (isMovie() || isShow()) { - parseOptions().then(async() => await init()); -} +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'amazon' }))(); diff --git a/src/sites/common.css b/src/sites/common.css index e29cb44..3479a6c 100644 --- a/src/sites/common.css +++ b/src/sites/common.css @@ -116,8 +116,13 @@ background: #2A2AFF !important; } -/* Web to Plex warning notifications */ -.web-to-plex-notification.warning { +/* Web to Plex success notifications */ +.web-to-plex-notification.success { + background: #03BDF9 !important; +} + +/* Web to Plex error/warning notifications */ +.web-to-plex-notification.warning, .web-to-plex-notification.error { background: #FF2A2A !important; } @@ -218,7 +223,17 @@ max-width: 60% !important; } -.web-to-plex-prompt-option.mutable > *:last-child { +.web-to-plex-prompt-option.mutable > h2 { + background: #0000 !important; + color: inherit !important; + font-family: inherit !important; + font-size: initial !important; + text-align: inherit !important; + + margin: inherit !important; +} + +.web-to-plex-prompt-option.mutable > .remove { background: #ffffff40 !important; border-radius: 3px !important; transition: all 0.1s !important; @@ -228,18 +243,30 @@ float: right !important; margin-right: -9px !important; - margin-top: -9px !important; + margin-top: -42px !important; padding: 0 !important; } -.web-to-plex-prompt-option.mutable > *:last-child:hover { +.web-to-plex-prompt-option.mutable > .remove:hover { background: #ffffff4d !important; } -.web-to-plex-prompt-option.mutable > *:last-child:after { +.web-to-plex-prompt-option.mutable > .remove::after { content: '\00d7' !important; } +.web-to-plex-prompt-option.mutable > .quality { + width: 50% !important; +} + +.web-to-plex-prompt-option.mutable > .location { + width: 90% !important; +} + +.web-to-plex-prompt-option.mutable > .location:last-child:not(:first-child) { + margin-top: 5px !important; +} + .web-to-plex-prompt-footer { text-align: right !important; border-bottom-left-radius: 3px !important; @@ -300,8 +327,11 @@ left: 5px !important; padding: 10px !important; position: fixed !important; + right: unset !important; z-index: 999999 !important; + min-height: 0 !important; + min-width: 0 !important; height: 72px !important; width: 180px !important; @@ -313,7 +343,7 @@ display: initial !important; } -.web-to-plex-button.hide:not(:hover) { +.web-to-plex-button.hide:not(:hover), .web-to-plex-button.sleeper { opacity: 0.1; } @@ -363,14 +393,18 @@ outline: none !important; } -.web-to-plex-button.wtp--found::after, .web-to-plex-button.wtp--found::before { - background: #F9BD03 !important; -} - .web-to-plex-button.wtp--download::after, .web-to-plex-button.wtp--download::before { background: #265AF4 !important; } +.web-to-plex-button.wtp--queued::after, .web-to-plex-button.wtp--queued::before { + background: #568AF4 !important; +} + +.web-to-plex-button.wtp--found::after, .web-to-plex-button.wtp--found::before { + background: #F9BD03 !important; +} + .web-to-plex-button.wtp--error::after, .web-to-plex-button.wtp--error::before { background: #FF2A2A !important; } @@ -485,3 +519,104 @@ padding: 3px 6px !important; position: absolute !important; } + +/* bbodine @CodePen - https://codepen.io/bbodine1/pen/novBm */ +.checkbox { + width: 80px; + height: 26px; + background: #000; + margin: 15px 0; + position: relative; + border-radius: 50px; + box-shadow: inset 0px 1px 1px rgba(0, 0, 0, 0.5), 0px 1px 0px rgba(255, 255, 255, 0.2); +} + +span.checkbox { + display: inline-block; + + margin: 0; + vertical-align: text-bottom; +} + +.checkbox::after { + content: 'OFF'; + color: #666; + position: absolute; + right: 10px; + z-index: 0; + font: 12px/26px Arial, sans-serif; + font-weight: bold; + text-shadow: 1px 1px 0px rgba(255, 255, 255, 0.15); +} + +.checkbox::before { + content: 'ON'; + color: #cc7b19; + position: absolute; + left: 10px; + z-index: 0; + font: 12px/26px Arial, sans-serif; + font-weight: bold; +} + +.checkbox[prompt-yes]::before { + content: attr(prompt-yes); + text-transform: uppercase; +} + +.checkbox[prompt-no]::after { + content: attr(prompt-no); + text-transform: uppercase; +} + +.checkbox[prompt-size="large"i]::before, .checkbox[prompt-size="large"i]::after { + font-size: 30px !important; +} + +.checkbox[prompt-size="medium"i]::before, .checkbox[prompt-size="medium"i]::after { + font-size: 21px !important; +} + +.checkbox[prompt-size="normal"i]::before, .checkbox[prompt-size="normal"i]::after { + font-size: 12px !important; +} + +.checkbox[prompt-size="small"i]::before, .checkbox[prompt-size="small"i]::after { + font-size: 6px !important; +} + +.checkbox[prompt="y/n"i]::before { + content: 'YES'; +} + +.checkbox[prompt="y/n"i]::after { + content: 'NO'; +} + +.checkbox label { + display: block; + width: 34px; + height: 20px; + cursor: pointer; + position: absolute; + top: 3px; + left: 3px; + z-index: 1; + background: #666; + border-radius: 50px; + transition: all 0.4s ease; + box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.3); +} + +.checkbox input[type=checkbox] { + visibility: hidden; +} + +.checkbox input[type=checkbox]:checked + label { + left: 43px; + background: #cc7b19; +} + +.checkbox[disabled] { + opacity: 0.25 !important; +} diff --git a/src/sites/common.js b/src/sites/common.js new file mode 100644 index 0000000..bf8f6f4 --- /dev/null +++ b/src/sites/common.js @@ -0,0 +1,5 @@ +/* global Update(type:string, details:object) */ +if(init && typeof init == 'function') + /* Do nothing */; +else + (init = () => Update('PLUGIN', { instance_type: 'PLUGIN', plugin: location.hostname.replace(/(?:[\w\-]+\.)?([^\.]+)(?:\.[^\\\/]+)/, '$1') }))(); diff --git a/src/sites/couchpotato/index.js b/src/sites/couchpotato/index.js index 64a9de5..7ef46cf 100644 --- a/src/sites/couchpotato/index.js +++ b/src/sites/couchpotato/index.js @@ -1,52 +1,2 @@ -/* global wait, modifyPlexButton, parseOptions, findPlexMedia */ -function init() { - wait( - () => document.querySelector('.media-body .clearfix') && document.querySelector('.media-body .clearfix').children.length > 1, - () => initPlexThingy(isMovie()? 'movie': isShow()? 'show': null) - ); -} - -function isMovie() { - return /^\/movies?\//.test(window.location.pathname); -} - -function isShow() { - return /^\/shows?\//.test(window.location.pathname); -} - -function initPlexThingy(type) { - let button = renderPlexButton(); - - if (!button || !type) - return /* Fatal Error: Fail Silently */; - - let $title = document.querySelector('[itemprop="description"]'), - $date = $title.previousElementSibling, - $image = document.querySelector('img[src*="wp-content"]'); - - if (!$title || !$date) - return modifyPlexButton( - $button, - 'error', - 'Could not extract title or year from CouchPotato' - ); - - let title = $title.textContent.trim(), - year = $date.textContent.trim(), - image = ($image || {}).src, - IMDbID = getIMDbID(); - - findPlexMedia({ title, year, image, button, type, IMDbID }); -} - -function getIMDbID() { - let $link = document.querySelector( - '[href*="imdb.com/title/tt"]' - ); - if ($link) { - let link = $link.href.replace(/^.*imdb\.com\/title\//, ''); - return link.replace(/\/(?:maindetails\/?)?$/, ''); - } -} - -parseOptions().then(init); +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'couchpotato' }))(); diff --git a/src/sites/fandango/index.js b/src/sites/fandango/index.js index 89e6444..366bb50 100644 --- a/src/sites/fandango/index.js +++ b/src/sites/fandango/index.js @@ -1,42 +1,2 @@ -/* global findPlexMedia, parseOptions, modifyPlexButton */ -function isMovie() { - return /\/movie-overview\/?$/.test(window.location.pathname); -} - -async function initPlexThingy(type) { - let $parent = document.querySelector('.subnav ul'), - button = renderPlexButton(); - - if (!button) - return /* Fatal Error: Fail Silently */; - - let $title = document.querySelector('.subnav__title'), - $year = document.querySelector('.movie-details__release-date'), - $image = document.querySelector('.movie-details__movie-img'); - - if (!$title || !$year) - return modifyPlexButton( - button, - 'error', - 'Could not extract title or year from Fandango' - ), - null; - - let title = $title.textContent.trim().split(/\n+/)[0].trim(), - year = $year.textContent.replace(/.*(\d{4}).*/, '$1').trim(), - image = ($image || {}).src; - - let Db = await getIDs({ title, year, type }), - IMDbID = Db.imdb, - TMDbID = Db.tmdb, - TVDbID = Db.tvdb; - - title = Db.title; - year = Db.year; - - findPlexMedia({ type, title, year, image, button, IMDbID, TMDbID, TVDbID }); -} - -if (isMovie()) { - parseOptions().then(async() => await initPlexThingy('movie')); -} +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'fandango' }))(); diff --git a/src/sites/flenix/index.js b/src/sites/flenix/index.js deleted file mode 100644 index 229a5f5..0000000 --- a/src/sites/flenix/index.js +++ /dev/null @@ -1,62 +0,0 @@ -/* global parseOptions, modifyPlexButton, findPlexMedia */ -function isMoviePage() { - // An example movie page: /movies/3030-the-1517-to-paris.html - return /\/(movies?|views?)\//.test(window.location.pathname); -} - -function isMoviePageReady() { - return !!document.querySelector('#videoplayer video').getAttribute('onplay') != ''; -} - -function init() { - if (isMoviePage()) - if (isMoviePageReady()) - initPlexThingy(); - else - // This almost never happens, but sometimes the page is too slow so we need to wait a bit. - setTimeout(init, 1000); -} - -parseOptions().then(() => { - window.addEventListener('popstate', init); - window.addEventListener('pushstate-changed', init); - init(); -}); - -async function initPlexThingy() { - let button = renderPlexButton(); - - if (!button) - return /* Fatal Error: Fail Silently */; - - let $title = document.querySelector('#dle-content .about > h1'), - $date = document.querySelector('.features > .reset:nth-child(2) a'); - - if (!$title || !$date) - return modifyPlexButton( - button, - 'error', - 'Could not extract title or year from Flenix' - ), - null; - - let meta = { - method: 'POST', - headers: {'Content-Type': 'application/x-www-form-urlencoded'}, - mode: 'no-cors' - }; - - let title = $title.innerText.trim(), - year = $date.innerText, - type = 'movie'; - - let Db = await getIDs({ title, year, type }), - IMDbID = Db.imdb, - TMDbID = Db.tmdb, - TVDbID = Db.tvdb; - - title = Db.title; - year = Db.year; - - findPlexMedia({ title, year, button, IMDbID, TMDbID, TVDbID, type, remote: '/engine/ajax/get.php', locale: 'flenix' }); -} diff --git a/src/sites/flickmetrix/index.js b/src/sites/flickmetrix/index.js index 1119254..4cbc2f3 100644 --- a/src/sites/flickmetrix/index.js +++ b/src/sites/flickmetrix/index.js @@ -1,106 +1,2 @@ -/* global wait, modifyPlexButton, parseOptions, findPlexMedia */ -function isMovie() { - return !!document.querySelector('#singleFilm') || /\bid=\d+\b/i.test(window.location.search); -} - -function isList() { - return !isMovie(); -} - -let START = +(new Date); - -function init() { - if(/\/(?=((?:watchlist|seen|favourites|trash)\b|$))/i.test(window.location.pathname)) - wait( - () => (!document.querySelector('#loadingOverlay > *')), - () => (isList()? initList: initPlexThingy)() - ); -} - -function initPlexThingy() { - let button = renderPlexButton(); - - if (!button) - return /* Fatal Error: Fail Silently */; - - let $element = document.querySelector('#singleFilm'), - $title = $element.querySelector('.title'), - $date = $element.querySelector('.title + *'), - $image = $element.querySelector('img'); - - if (!$title || !$date) - return modifyPlexButton( - button, - 'error', - 'Could not extract title or year from Flickmetrix' - ); - - let title = $title.textContent.trim(), - year = $date.textContent.replace(/^\(|\)$/g, '').trim(), - image = ($image || {}).src, - IMDbID = getIMDbID($element); - - findPlexMedia({ title, year, button, type: 'movie', IMDbID }); -} - -function getIMDbID(element) { - let $link = element.querySelector( - '[href*="imdb.com/title/tt"]' - ); - if ($link) { - let link = $link.href.replace(/^.*imdb\.com\/title\//, ''); - return link.replace(/\/(?:maindetails\/?)?$/, ''); - } -} - -async function addInListItem(element) { - let $title = element.querySelector('.title'), - $date = element.querySelector('.title + *'), - $image = element.querySelector('img'); - - if (!$title) - return; - - let title = $title.textContent.trim(), - year = +$date.textContent.replace(/^\(|\)$/g, '').trim(), - image = $image.src, - type = 'movie', - IMDbID = getIMDbID(element); - - let Db = await getIDs({ type, title, year, IMDbID }), - TMDbID = Db.tmdb, - TVDbID = Db.tvdb; - - title = title || Db.title; - year = year || Db.year; - IMDbID = IMDbID || Db.IMDbID; - - return { type, title, year, image, IMDbID, TMDbID, TVDbID }; -} - -function initList() { - let $listItems = document.querySelectorAll('.film'), - button = renderPlexButton(), - options = [], length = $listItems.length - 1; - - if (!button) - return /* Fatal Error: Fail Silently */; - - $listItems.forEach(async(element, index, array) => { - let option = await addInListItem(element); - - if(option) - options.push(option); - - if(index == length) - setTimeout(() => { - terminal.log(options) - if (!options.length) - new Notification('error', 'Failed to process list'); - else - squabblePlex(options, button); - }, 50); - }); -} - -parseOptions().then(window.onlocationchange = init); +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'flickmetrix' }))(); diff --git a/src/sites/google/index.js b/src/sites/google/index.js index db88def..1e3c09f 100644 --- a/src/sites/google/index.js +++ b/src/sites/google/index.js @@ -1,72 +1,2 @@ -function isMovie() { - return document.querySelector('#media_result_group, [href*="themoviedb.org/tv/"], [href*="imdb.com/title/tt"]'); -} - -function isShow() { - return document.queryBy('[href*="thetvdb.com/"][href*="id="], [href*="thetvdb.com/series/"], [href*="themoviedb.org/tv/"], [href*="imdb.com/title/tt"][href$="externalsites"]').first; -} - -function init() { - if(isMovie() || isShow()) - initPlexThingy(isMovie()? 'movie': isShow()? 'show': null); -} - -async function initPlexThingy(type) { - let $title, $type, $date, $image; - - let button = renderPlexButton(); - if(!button || !type) - return /* Fail silently */; - - if(type == 'movie') { - $title = document.querySelector('.kno-ecr-pt'); - $type = document.querySelector('.kno-ecr-pt + *'); // in case a tv show is incorrectly identified - $date = document.querySelector('.kno-fb-ctx:not([data-local-attribute]) span'); - $image = document.querySelector('#media_result_group img'); - } else { - $title = isShow().querySelector('*'); - $date = { textContent: '' }; - $image = { src: '' }; - } - - if(!$title || !$date) - return modifyPlexButton(button, 'error', 'Could not extract title or year from Google'); - - if($type) { - type = $type.textContent; - - type = /\b(tv|show|series)\b/i.test(type)? 'show': /\b(movie|film|cinema|(?:\d+h\s+)?\d+m)\b/i.test(type)? 'movie': 'error'; - $date = (type == 'show'? document.querySelector('.kno-fv') || $date: $date) || { textContent: '' }; - } - - if(type == 'error') - return; - - let date = $date.textContent.replace(/(\d{4})/), - year = +RegExp.$1, - title = $title.textContent.replace((type == 'movie'? /^(.+)$/: /(.+)(?:(?:\:\s*series\s+info|\-\s*(?:all\s+episodes|season)).+)$/i), '$1').trim(), - image = ($image || {}).src; - - year = year > 999? year: 0; - - let IMDbID = getIMDbID(), - Db = await getIDs({ title, year, type, IMDbID }), - TMDbID = Db.tmdb, - TVDbID = Db.tvdb; - - findPlexMedia({ type, title, year, image, button, IMDbID, TMDbID, TVDbID }); -} - -function getIMDbID() { - let link = document.querySelector('a._hvg[href*="imdb.com/title/tt"]'); - - if(link) - return link.href.replace(/.*(tt\d+).*/, '$1'); -} - -parseOptions() - .then(() => { - window.addEventListener('popstate', init); - window.addEventListener('pushstate-changed', init); - init(); - }); +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'google' }))(); diff --git a/src/sites/google/play.js b/src/sites/google/play.js index 7c59064..0d53cab 100644 --- a/src/sites/google/play.js +++ b/src/sites/google/play.js @@ -1,50 +1,2 @@ -/* global wait, modifyPlexButton, parseOptions, findPlexMedia */ -function isMoviePage() { - return window.location.pathname.startsWith('/store/movies/'); -} - -function isShowPage() { - return window.location.pathname.startsWith('/store/tv/'); -} - -function init() { - if (isMoviePage() || isShowPage()) { - wait( - () => document.querySelector('c-wiz span > button.id-track-click'), - () => initPlexThingy(isMoviePage() ? 'movie' : 'tv') - ); - } -} - -async function initPlexThingy(type) { - let button = renderPlexButton(); - - if (!button) - return /* Fatal Error: Fail Silently */; - - let $title = document.querySelector('h1'), - $year = document.querySelector(`h1 ~ div span:${ type === 'movie'? 'first': 'last' }-of-type`), - $image = document.querySelector('img[alt="cover art" i]'); - - if (!$title || !$year) - return modifyPlexButton(button, 'error', `Could not extract ${ !$title? 'title': 'year' } from Google`); - - let title = $title.textContent.replace(/\(\s*(\d{4})\s*\).*?$/, '').trim(), - year = (RegExp.$1 || $year.textContent).replace(/^.*?(\d+)/, '$1').trim(), - image = ($image || {}).src, - Db = await getIDs({ title, year, type }), - IMDbID = Db.imdb, - TMDbID = Db.tmdb, - TVDbID = Db.tvdb; - - title = Db.title; - year = Db.year; - - findPlexMedia({ type, title, year, button, IMDbID, TMDbID, TVDbID }); -} - -parseOptions().then(() => { - window.addEventListener('popstate', init); - window.addEventListener('pushstate-changed', init); - init(); -}); +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'google.play' }))(); diff --git a/src/sites/gostream/index.js b/src/sites/gostream/index.js index b8d1f3b..cc74b85 100644 --- a/src/sites/gostream/index.js +++ b/src/sites/gostream/index.js @@ -1,63 +1,2 @@ -/* global parseOptions, modifyPlexButton, findPlexMedia */ -function isMoviePage() { - // An example movie page: /movies/3030-the-1517-to-paris.html - return /\/(?!genre|most-viewed|top-imdb|contact)\b/.test(window.location.pathname); -} - -function isMoviePageReady() { - let e = document.querySelector('.movieplay iframe, .desc iframe'); - return !!e && e.src != '' && document.readyState == 'complete'; -} - -function init() { - if (isMoviePage()) - if (isMoviePageReady()) - initPlexThingy(); - else - // This almost never happens, but sometimes the page is too slow so we need to wait a bit. - setTimeout(init, 1000); -} - -parseOptions().then(() => { - window.addEventListener('popstate', init); - window.addEventListener('pushstate-changed', init); - init(); -}); - -async function initPlexThingy() { - - let button = renderPlexButton(); - if (!button) - return /* an error occurred, fail silently */; - - let $title = document.querySelector('[itemprop="name"]:not(meta)'), - $year = document.querySelector('.mvic-desc [href*="year/"]'), - $image, start = +(new Date); - - wait(() => (+(new Date) - start > 5000) || ($image = document.querySelector('.hiddenz, [itemprop="image"]'))); - - if (!$title) - return modifyPlexButton( - button, - 'error', - 'Could not extract title from GoStream' - ), - null; - - new Notification('update', 'Select the Openload (OL) server to download'); - - let title = $title.innerText.trim(), - year = ($year? $year.innerText.trim(): 0), - image = ($image? $image.src: null), - type = 'movie'; - - let Db = await getIDs({ title, year, type }), - IMDbID = Db.imdb, - TMDbID = Db.tmdb, - TVDbID = Db.tvdb; - - title = Db.title; - year = Db.year; - - findPlexMedia({ title, year, image, button, IMDbID, TMDbID, TVDbID, type }); -} +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'gostream' }))(); diff --git a/src/sites/hulu/index.js b/src/sites/hulu/index.js index 6e1d3e0..9b777ed 100644 --- a/src/sites/hulu/index.js +++ b/src/sites/hulu/index.js @@ -1,39 +1,2 @@ -/* global findPlexMedia, parseOptions, modifyPlexButton */ -function isReady() { - return $$('#content [class$="__meta"]'); -} - -function isMovie() { - return window.location.pathname.startsWith('/movie/'); // /movies/ is STRICTLY for a collection of movies (e.g. the line-up) -} - -function isShow() { - return window.location.pathname.startsWith('/series/'); // /tv/ is STRICTLY for a collection of movies (e.g. the line-up) -} - -let $$ = selector => document.querySelector(selector); - -async function initPlexThingy(type) { - let button = renderPlexButton(); - - if (!button || !type) - return /* Fatal Error: Fail Silently */; - - let $title = $$('#content [class$="__name"]'), - $year = $$('#content [class$="__meta"] [class$="segment"]:last-child'), - title = $title.innerText.replace(/^\s+|\s+$/g, '').toCaps(), - year = +$year.textContent.replace(/.*\((\d{4})\).*/, '$1'), - Db = await getIDs({ title, year, type }), - IMDbID = Db.imdb, - TMDbID = Db.tmdb, - TVDbID = Db.tvdb; - - title = Db.title; - year = Db.year; - - findPlexMedia({ type, title, year, button, IMDbID, TMDbID, TVDbID }); -} - -(window.onlocationchange = () => - wait(isReady, () => parseOptions().then(async() => await initPlexThingy(isMovie()? 'movie': isShow()? 'tv': null))) -)(); +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'hulu' }))(); diff --git a/src/sites/imdb/index.js b/src/sites/imdb/index.js index 24ec8f7..1c4ef9a 100644 --- a/src/sites/imdb/index.js +++ b/src/sites/imdb/index.js @@ -1,183 +1,2 @@ -/* global findPlexMedia, parseOptions, modifyPlexButton */ -function isMovie() { - let tag = $$('meta[property="og:type"]'); - return tag && tag.content === 'video.movie'; -} - -function isShow() { - let tag = $$('meta[property="og:type"]'); - return tag && tag.content === 'video.tv_show'; -} - -function isList() { - return window.location.pathname.startsWith('/list/'); -} - -function getIMDbID() { - let tag = $$('meta[property="pageId"]'); - return tag ? tag.content : undefined; -} - -let $$ = (selector, index = 0) => document.queryBy(selector)[index], - IMDbID = getIMDbID(), - usa = /\b(USA?|United\s+States)\b/i; - -function cleanYear(year) { - // The year can contain `()`, so we need to strip it out. - return year.replace(/^\(|\)$/g, '').trim(); -} - -async function initPlexMovie() { - let $parent = $$('.plot_summary'), - button = renderPlexButton(), - type = 'movie'; - - if (!button) - return /* Fatal Error: Fail Silently */; - - let $title = $$('.originalTitle, .title_wrapper h1'), - $altname = $$('.title_wrapper h1'), - $date = $$('.title_wrapper [href*="/releaseinfo"]'), - $year = $$('.title_wrapper #titleYear'), - $image = $$('img[alt$="poster" i]'), - // TODO: Hmm there should be a less risky way... - title = $title.childNodes[0].textContent.trim(), - altname = ($altname == $title? title: $altname.childNodes[0].textContent.trim()), - country = $date.innerText.replace(/[^]+\((\w+)\)[^]*?$/, '$1'), - year = +cleanYear($year.textContent), - image = ($image || {}).src; - title = usa.test(country)? title: altname; - - let Db = await getIDs({ title, year, type, IMDbID }), - TMDbID = +Db.tmdb, - TVDbID = +Db.tvdb; - - IMDbID = IMDbID || Db.imdb; - title = Db.title; - year = Db.year; - - findPlexMedia({ type, title, year, image, button, IMDbID, TMDbID, TVDbID }); -} - -async function initPlexShow() { - let $parent = $$('.plot_summary'), - button = renderPlexButton(), - type = 'show'; - - if (!button) - return /* Fatal Error: Fail Silently */; - - let $title = $$('.originalTitle, .title_wrapper h1'), - $altname = $$('.title_wrapper h1'), - $date = $$('.title_wrapper [href*="/releaseinfo"]'), - date = $$('title').textContent, - dateMatch = date.match(/Series\s*\(?(\d{4})(?:[^\)]+\))?/i), - $image = $$('img[alt$="poster" i]'); - - if (!($title || $altname) || !dateMatch) - return modifyPlexButton(button, 'error', `Could not extract ${ !($title || $altname)? 'title': 'year' } from IMDb`); - - let title = $title.textContent.trim(), - altname = ($altname == $title? title: $altname.childNodes[0].textContent.trim()), - country = $date.innerText.replace(/[^]+\((\w+)\)[^]*?$/, '$1'), - year = parseInt(dateMatch[1]), - image = ($image || {}).src; - title = usa.test(country)? title: altname; - - let Db = await getIDs({ title, year, type, IMDbID }), - TMDbID = +Db.tmdb, - TVDbID = +Db.tvdb; - - IMDbID = IMDbID || Db.imdb; - title = Db.title; - year = Db.year; - - let savename = title.toLowerCase(), - cached = await load(`${savename}.imdb`); - - if(!cached) { - save(`${savename} (${year}).imdb`, { type, title, year, imdb: IMDbID, tmdb: TMDbID, tvdb: TVDbID }); - save(`${savename}.imdb`, +year); - terminal.log(`Saved as "${savename} (${year}).imdb"`); - } - - findPlexMedia({ type, title, year, button, IMDbID, TMDbID, TVDbID }); -} - -async function addInListItem(element) { - let $title = element.querySelector('.col-title a'), - $date = element.querySelector('.col-title a + *'), - $image = element.querySelector('img.loadlate, img[data-tconst]'), - $IMDbID = $title; - - if (!$title || !$date) - return; - - let title = $title.textContent.trim(), - year = cleanYear($date.textContent), - image = $image.src, - IMDbID = $IMDbID.href.replace(/.*\/(tt\d+)\b.*$/, '$1'), - type = (/[\-\u2010-\u2015]/.test(year)? 'show': 'movie'); - - year = parseInt(year); - - let Db = await getIDs({ type, title, year, IMDbID }), - TMDbID = +Db.tmdb, - TVDbID = +Db.tvdb; - - title = title || Db.title; - year = year || Db.year; - - let savename = title.toLowerCase(), - cached = await load(`${savename}.imdb`); - - if(!cached) { - save(`${savename} (${year}).imdb`, { type, title, year, imdb: IMDbID, tmdb: TMDbID, tvdb: TVDbID }); - save(`${savename}.imdb`, +year); - terminal.log(`Saved as "${savename} (${year}).imdb"`); - } - - return { type, title, year, image, IMDbID, TMDbID, TVDbID }; -} - -function initList() { - let $listItems = document.querySelectorAll('#main .lister-item'), - button = renderPlexButton(true), - options = [], length = $listItems.length - 1; - - if (!/&mode=simple/i.test(location.search)) - return location.search = location.search.replace(/&mode=\w+/, '&mode=simple'); - - if (!button) - return /* Fatal Error: Fail Silently */; - - $listItems.forEach(async(element, index, array) => { - let option = await addInListItem(element); - - if(option) - options.push(option); - - if(index == length) - setTimeout(() => { - if (!options.length) - new Notification('error', 'Failed to process list'); - else - squabblePlex(options, button); - }, 50); - }); -} - -let init = () => { - if (((isMovie() || isShow()) && IMDbID) || isList()) { - parseOptions().then(async() => { - if (isMovie()) - await initPlexMovie(); - else if (isShow()) - await initPlexShow(); - else if(isList()) - await initList(); - }); - } -} - -init(); +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'imdb' }))(); diff --git a/src/sites/itunes/index.js b/src/sites/itunes/index.js index 3bfe34e..4f9ec1b 100644 --- a/src/sites/itunes/index.js +++ b/src/sites/itunes/index.js @@ -1,45 +1,2 @@ -/* global findPlexMedia, parseOptions, modifyPlexButton */ -function isMovie() { - return /^(\/\w+)?\/movie\//.test(window.location.pathname); -} - -function isShow() { - return /^(\/\w+)?\/tv-season\//.test(window.location.pathname); -} - -let $$ = selector => document.querySelector(selector); - -async function initPlexThingy(type) { - let title, year, image, button = renderPlexButton(); - - if (!button || !type) - return /* Fatal Error: Fail Silently */; - - if(type == 'movie') { - let $title = $$('[class*="movie-header__title"]'), - $year = $$('[datetime]'), - $image = $$('[class*="product"] ~ * picture img'); - - title = $title.textContent; - year = +$year.textContent; - image = ($image || {}).src; - } else { - let meta = [$$('h1[itemprop="name"], h1'), $$('.release-date > *:last-child'), $$('[class*="product"] ~ * picture img')]; - - title = meta[0].textContent.replace(/\s*\((\d+)\)\s*/, '').trim(); - year = meta[1].textContent.replace(/[^]*(\d{4})[^]*?$/g, '$1').trim(); - image = meta[2].src; - } - - let Db = await getIDs({ title, year, type }), - IMDbID = Db.imdb, - TMDbID = Db.tmdb, - TVDbID = Db.tvdb; - - title = Db.title; - year = Db.year; - - findPlexMedia({ type, title, year, image, button, IMDbID, TMDbID, TVDbID }); -} - -parseOptions().then(async() => await initPlexThingy(isMovie()? 'movie': isShow()? 'tv': null)); +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'itunes' }))(); diff --git a/src/sites/justwatch/index.css b/src/sites/justwatch/index.css new file mode 100644 index 0000000..e69de29 diff --git a/src/sites/justwatch/index.js b/src/sites/justwatch/index.js new file mode 100644 index 0000000..b5dff44 --- /dev/null +++ b/src/sites/justwatch/index.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'justwatch' }))(); diff --git a/src/sites/layout.js b/src/sites/layout.js deleted file mode 100644 index ea23844..0000000 --- a/src/sites/layout.js +++ /dev/null @@ -1,34 +0,0 @@ -/* Your file should look something similar to this */ -/* required: function init */ -function init( type ) { - if ( PageReady() ) - startWebtoPlex( type ); - else - setTimeout(init, 1000); -} - -function PageReady() { - // should return a boolen/object to indicate the page is finished loading - return document.readyState == 'complete'; -} - -async function startWebtoPlex(type) { - let button = renderButton(); - if (!button) - return /* Silent error */; - - let title = document.querySelector('#title').textContent, - year = document.querySelector('#year').textContent, - image = document.querySelector('#poster').textContent; - - let Db = await getIDs({ title, year, type }), - IMDbID = Db.imdb, - TMDbID = Db.tmdb, - TVDbID = Db.tvdb; - - findPlexMedia({ title, year, image, button, IMDbID, TMDbID, TVDbID, type }); -} - -parseOptions().then(() => { - init( location.pathname.startsWith('/movie')? 'movie': 'show' ) -}); diff --git a/src/sites/letterboxd/index.js b/src/sites/letterboxd/index.js index 60d719a..80cd852 100644 --- a/src/sites/letterboxd/index.js +++ b/src/sites/letterboxd/index.js @@ -1,99 +1,2 @@ -/* global wait, modifyPlexButton, parseOptions, findPlexMedia */ -function isList() { - return /\/list\//i.test(window.location.pathname); -} - -let START = +(new Date); - -function init() { - if(/\/(film|list)\//i.test(window.location.pathname)) - wait( - () => (isList()? +(new Date) - START > 500: document.querySelector('.js-watch-panel')), - () => ((isList()? initList: initPlexThingy)()) - ); -} - -function initPlexThingy() { - let button = renderPlexButton(); - - if (!button) - return /* Fatal Error: Fail Silently */; - - let $title = document.querySelector('.headline-1[itemprop="name"]'), - $date = document.querySelector('small[itemprop="datePublished"]'), - $image = document.querySelector('.image'); - - if (!$title || !$date) - return modifyPlexButton( - button, - 'error', - 'Could not extract title or year from Letterboxd' - ); - - let title = $title.textContent.trim(), - year = $date.textContent.trim(), - image = ($image || {}).src, - IMDbID = getIMDbID(); - - findPlexMedia({ title, year, button, type: 'movie', IMDbID }); -} - -function getIMDbID() { - let $link = document.querySelector( - '.track-event[href*="imdb.com/title/tt"]' - ); - if ($link) { - let link = $link.href.replace(/^.*imdb\.com\/title\//, ''); - return link.replace(/\/(?:maindetails\/?)?$/, ''); - } -} - -async function addInListItem(element) { - let $title = element.querySelector('.frame-title'), - $image = element.querySelector('img'); - - if (!$title) - return; - - let title = $title.textContent.replace(/\((\d+)\)/, '').trim(), - year = +RegExp.$1, - image = $image.src, - type = 'movie'; - - let Db = await getIDs({ type, title, year }), - IMDbID = Db.imdb, - TMDbID = Db.tmdb, - TVDbID = Db.tvdb; - - title = title || Db.title; - year = year || Db.year; - - return { type, title, year, image, IMDbID, TMDbID, TVDbID }; -} - -function initList() { - let $listItems = document.querySelectorAll('.poster-list .poster-container'), - button = renderPlexButton(true), - options = [], length = $listItems.length - 1; - - if (!button) - return /* Fatal Error: Fail Silently */; - - $listItems.forEach(async(element, index, array) => { - let option = await addInListItem(element); - - if(option) - options.push(option); - - if(index == length) - setTimeout(() => { - terminal.log(options) - if (!options.length) - new Notification('error', 'Failed to process list'); - else - squabblePlex(options, button); - }, 50); - }); -} - -parseOptions().then(init); +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'letterboxd' }))(); diff --git a/src/sites/metacritic/index.js b/src/sites/metacritic/index.js index 07eb064..be3e7c5 100644 --- a/src/sites/metacritic/index.js +++ b/src/sites/metacritic/index.js @@ -1,62 +1,2 @@ -/* global wait, modifyPlexButton, parseOptions, findPlexMedia */ -function init() { - wait( - () => document.readyState === 'complete', - () => initPlexThingy(isMovie()? 'movie': isShow()? 'tv': null) || isList()? initList(): null - ); -} - -function isMovie() { - return /^\/movie\//i.test(window.location.pathname); -} - -function isShow() { - return /^\/tv\//i.test(window.location.pathname); -} - -function isList() { - return /(^\/list\/)/i.test(window.location.pathname); -} - -async function initPlexThingy(type) { - let button = renderPlexButton(); - - if (!button || !type) - return /* Fatal Error: Fail Silently */; - - let $title = document.querySelector('.product_page_title > *, .product_title'), - $date = document.querySelector('.product_page_title > .release_year, .product_data .release_data'), - $image = document.querySelector('.summary_img'); - - if (!$title || !$date) - return console.log('failed'), modifyPlexButton( - button, - 'error', - `Could not extract ${ !$title? 'title': 'year' } from Metacritic` - ); - - let title = $title.textContent.replace(/\s+/g, ' ').trim(), - year = $date.textContent.replace(/\s+/g, ' ').replace(/.*(\d{4}).*$/, '$1').trim(), - image = ($image || {}).src; - - let Db = await getIDs({ title, year, type }), - IMDbID = Db.imdb, - TMDbID = Db.tmdb, - TVDbID = Db.tvdb; - - title = Db.title; - year = Db.year; - - type = type === 'tv'? 'show': type; - - findPlexMedia({ title, year, button, type, IMDbID, TMDbID, TVDbID }); -} - -async function initList() { - /* Not implemented... Metacritic has too much sh*t loading to even try to open a console */ - /* Targeted for v5/v6 */ -} - -parseOptions().then(() => { - init(); -}); +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'metacritic' }))(); diff --git a/src/sites/moviemeter/index.css b/src/sites/moviemeter/index.css new file mode 100644 index 0000000..e69de29 diff --git a/src/sites/moviemeter/index.js b/src/sites/moviemeter/index.js new file mode 100644 index 0000000..4bf6acf --- /dev/null +++ b/src/sites/moviemeter/index.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'moviemeter' }))(); diff --git a/src/sites/movieo/index.js b/src/sites/movieo/index.js index fd7977c..7738986 100644 --- a/src/sites/movieo/index.js +++ b/src/sites/movieo/index.js @@ -1,128 +1,2 @@ -/* global parseOptions, modifyPlexButton, findPlexMedia */ -function isMoviePage() { - let path = window.location.pathname; - - if (!path.startsWith('/movies/')) - return false; - - // An example movie page: /movies/juno-hpsgt (can also have trailing slash!) - // Example non-movie page: /movies/watchlist/gbdx - // So if there is one slash extra (trailing slash not included), it's not a movie page. - let jup = path.replace('/movies/', '').slice(0, -1); - return !jup.includes('/'); -} - -function isList() { - let path = window.location.pathname; - - return /\/(black|seen|watch)?lists?\//i.test(path); -} - -function isPageReady() { - return !!document.querySelector('.share-box, .zopim'); -} - -function init() { - if (isMoviePage()) { - if (isPageReady()) { - initPlexThingy(); - } else { - // This almost never happens, but sometimes the page is too slow so we need to wait a bit. - // I could reproduce this by clicking on a movie in the movie watchlist, - // going back in history and then going forward in history. - setTimeout(init, 1000); - } - } else if (isList()) { - if (isPageReady()) { - initList(); - } else { - setTimeout(init, 1000); - } - } -} - -parseOptions().then(() => { - window.addEventListener('popstate', init); - window.addEventListener('pushstate-changed', init); - init(); -}); - -function initPlexThingy() { - let button = renderPlexButton(); - - if (!button) - return /* Fatal Error: Fail Silently */; - - let $title = document.querySelector('#doc_title'), - $date = document.querySelector('meta[itemprop="datePublished"]'), - $image = document.querySelector('img.poster'); - - if (!$title || !$date) - return modifyPlexButton( - button, - 'error', - `Could not extract ${ !$title? 'title': 'year' } from Movieo` - ); - - let title = $title.dataset.title.trim(), - year = $date.content.slice(0, 4), - image = ($image || {}).src, - IMDbID = getIMDbID(); - - findPlexMedia({ title, year, button, image, type: 'movie', IMDbID }); -} - -function getIMDbID() { - let $link = document.querySelector( - '.tt-parent[href*="imdb.com/title/tt"]' - ); - if ($link) - return $link.href.replace(/^[^]*\/title\//, ''); -} - -async function addInListItem(element) { - let $title = element.querySelector('.title'), - $image = element.querySelector('.poster-cont'); - - if (!$title) - return; - - let title = $title.textContent.trim().replace(/\s*\((\d{4})\)/, ''), - year = +RegExp.$1, - image = $image.getAttribute('data-src'), - type = 'movie'; - - let Db = await getIDs({ type, title, year }), - IMDbID = Db.imdb, - TMDbID = Db.tmdb, - TVDbID = Db.tvdb; - - title = title || Db.title; - year = year || Db.year; - - return { type, title, year, image, IMDbID, TMDbID, TVDbID }; -} - -function initList() { - let $listItems = document.querySelectorAll('[data-title][data-id]'), - button = renderPlexButton(true), - options = [], length = $listItems.length - 1; - - if (!button) - return /* Fatal Error: Fail Silently */; - - $listItems.forEach(async(element, index, array) => { - let option = await addInListItem(element); - - if(option) - options.push(option); - - if(index == length) - setTimeout(() => { - if (!options.length) - new Notification('error', 'Failed to process list'); - else - squabblePlex(options, button); - }, 50); - }); -} +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'movieo' }))(); diff --git a/src/sites/netflix/index.js b/src/sites/netflix/index.js index 72bded2..5bc66c2 100644 --- a/src/sites/netflix/index.js +++ b/src/sites/netflix/index.js @@ -1,42 +1,2 @@ -/* global findPlexMedia, parseOptions, modifyPlexButton */ -function isReady() { - let element = $$('[class$="__time"]'); - - return document.readyState == 'complete' && element && !/^([0:]+|null|undefined)?$/.test(element.textContent); -} - -function isMovie() { - return !isShow(); -} - -function isShow() { - return $$('[class*="playerEpisodes"]'); -} - -let $$ = selector => document.querySelector(selector); - -async function initPlexThingy(type) { - let button = renderPlexButton(); - - if (!button || !type) - return /* Fatal Error: Fail Silently */; - - let $title = $$('.video-title h4'), - title = $title.innerText.replace(/^\s+|\s+$/g, '').toCaps() || sessionStorage.getItem(`last-${type}-title`), - year = 0, - Db = await getIDs({ title, year, type }), - IMDbID = Db.imdb, - TMDbID = Db.tmdb, - TVDbID = Db.tvdb; - - title = Db.title; - year = Db.year; - - sessionStorage.setItem(`last-${type}-title`, title); - - findPlexMedia({ type, title, year, button, IMDbID, TMDbID, TVDbID }); -} - -(window.onlocationchange = () => - wait(isReady, () => parseOptions().then(async() => await initPlexThingy(isMovie()? 'movie': isShow()? 'tv': null))) -)(); +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'netflix' }))(); diff --git a/src/sites/plex/index.css b/src/sites/plex/index.css new file mode 100644 index 0000000..e69de29 diff --git a/src/sites/plex/index.js b/src/sites/plex/index.js new file mode 100644 index 0000000..f25c05c --- /dev/null +++ b/src/sites/plex/index.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'plex' }))(); diff --git a/src/sites/rottentomatoes/index.js b/src/sites/rottentomatoes/index.js index 28be81e..6fb798c 100644 --- a/src/sites/rottentomatoes/index.js +++ b/src/sites/rottentomatoes/index.js @@ -1,104 +1,2 @@ -/* global wait, modifyPlexButton, parseOptions, findPlexMedia */ -function init() { - wait( - () => (isList()? document.readyState === 'complete': document.querySelector('#reviews')), - () => (initPlexThingy(isMovie()? 'movie': isShow()? 'show': null) || isList()? initList(): null) - ); -} - -function isMovie() { - return /^\/m/.test(window.location.pathname); -} - -function isShow() { - return /^\/t/.test(window.location.pathname); -} - -function isList() { - return /^\/browse\//i.test(window.location.pathname); -} - -async function initPlexThingy(type) { - let button = renderPlexButton(); - - if (!button || !type) - return /* Fatal Error: Fail Silently */; - - let $title = document.querySelector('.playButton + .title, [itemprop="name"]'), - $year = (type == 'movie'? $title.nextElementSibling: $title.querySelector('.subtle')), - $image = document.querySelector('[class*="posterimage" i]'); - - if (!$title || !$year) - return modifyPlexButton( - button, - 'error', - 'Could not extract title or year from Rotten Tomatoes' - ); - - let title = $title.textContent.trim().replace(/(.+)\:[^]*$/, type == 'movie'? '$&': '$1'), - year = $year.textContent.replace(/\D+/g, '').trim(), - image = ($image || {}).srcset, - Db = await getIDs({ title, year, type }), - IMDbID = Db.imdb, - TMDbID = Db.tmdb, - TVDbID = Db.tvdb; - - if (image) - image = image.replace(/([^\s]+)[^]*/, '$1'); - - findPlexMedia({ title, year, image, button, type, IMDbID, TMDbID, TVDbID }); -} - -async function addInListItem(element) { - let $title = element.querySelector('.movieTitle'), - $image = element.querySelector('.poster'), - $type = element.querySelector('[href^="/m/"], [href^="/t/"]'); - - if (!$title) - return; - - let title = $title.textContent.trim(), - image = $image.src, - type = /\/([mt])\//i.test($type.href)? RegExp.$1 == 'm'? 'movie': 'show': null; - - if(!type) - return {}; - if(type == 'show') - title = title.replace(/\s*\:\s*seasons?\s+\d+\s*/i, ''); - - let Db = await getIDs({ type, title }), - IMDbID = Db.imdb, - TMDbID = Db.tmdb, - TVDbID = Db.tvdb, - year = Db.year; - - title = title || Db.title; - - return { type, title, year, image, IMDbID, TMDbID, TVDbID }; -} - -function initList() { - let $listItems = document.querySelectorAll('.mb-movie'), - button = renderPlexButton(true), - options = [], length = $listItems.length - 1; - - if (!button) - return /* Fatal Error: Fail Silently */; - - $listItems.forEach(async(element, index, array) => { - let option = await addInListItem(element); - - if(option) - options.push(option); - - if(index == length) - setTimeout(() => { - if (!options.length) - new Notification('error', 'Failed to process list'); - else - squabblePlex(options, button); - }, 50); - }); -} - -parseOptions().then(init); +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'rottentomatoes' }))(); diff --git a/src/sites/theme.css b/src/sites/theme.css new file mode 100644 index 0000000..6ad4146 --- /dev/null +++ b/src/sites/theme.css @@ -0,0 +1,38 @@ +/* Themes and other stylings for Web to Plex */ +/* Button Layout (Reference) + +// - an optional attribute value + +// The main button (container) +BUTTON .web-to-plex-button... + + // (Container) Holds the LIs that perform the various actions + UL + + // The Web to Plex (logo) button + LI #wtp-list-name.list-name + // The actionable anchor + A .list-action + IMG // Web to Plex logo + + // The Plex It! button + LI #wtp-plexit.list-item + IMG // Alarm bell + + // The hide button + LI #wtp-hide.list-item + IMG // Eye icon + + // The refresh button + LI #wtp-refresh.list-item + IMG // Refresh + + // The settings button + LI #wtp-options.list-item + IMG // Gear icon +*/ + +.web-to-plex-button.button-location-right { + left: unset !important; + right: 5px !important; +} diff --git a/src/sites/tmdb/index.js b/src/sites/tmdb/index.js index 9ca127a..6989aed 100644 --- a/src/sites/tmdb/index.js +++ b/src/sites/tmdb/index.js @@ -1,125 +1,2 @@ -/* global wait, modifyPlexButton, parseOptions, findPlexMedia */ -function init() { - wait( - () => document.readyState === 'complete', - () => (initPlexThingy(isMovie()? 'movie': isShow()? 'tv': null) || isList()? initList(): null) - ); -} - -function isMovie() { - return /\/movie\/\d+/i.test(window.location.pathname); -} - -function isShow() { - return /\/tv\/\d+/i.test(window.location.pathname); -} - -function isList() { - return /(^\/discover\/|\/(movie|tv)\/([^\d]+|\B))/i.test(window.location.pathname); -} - -async function initPlexThingy(type) { - let button = renderPlexButton(); - - if (!button || !type) - return /* Fatal Error: Fail Silently */; - - let $title = document.querySelector('.title > span > *:not(.release_date)'), - $date = document.querySelector('.title .release_date'), - $image = document.querySelector('img.poster'); - - if (!$title || !$date) - return modifyPlexButton( - button, - 'error', - `Could not extract ${ !$title? 'title': 'year' } from TheMovieDb` - ); - - let title = $title.textContent.trim(), - year = $date.textContent.trim(), - image = ($image || {}).src, - apid = window.location.pathname.replace(/\/(?:movie|tv)\/(\d+).*/, '$1'); - - type = type == 'movie'? 'movie': 'show'; - - let Db = await getIDs({ title, year, TMDbID: apid, APIType: type, APIID: apid }), - IMDbID = Db.imdb, - TMDbID = +Db.tmdb, - TVDbID = +Db.tvdb; - - title = Db.title; - year = Db.year; - - let savename = title.toLowerCase(), - cached = await load(`${savename}.tmdb`); - - if(!cached) { - save(`${savename} (${year}).tmdb`, { type, title, year, imdb: IMDbID, tmdb: TMDbID, tvdb: TVDbID }); - save(`${savename}.tmdb`, +year); - terminal.log(`Saved as "${savename} (${year}).tmdb"`); - } - - findPlexMedia({ title, year, image, button, type, IMDbID, TMDbID, TVDbID }); -} - -async function addInListItem(element) { - let $title = element.querySelector('.title'), - $date = element.querySelector('.title + *'), - $image = element.querySelector('.poster'), - $type = $title.id.split('_'); - - if (!$title || !$date) - return; - - let title = $title.textContent.trim(), - year = $date.textContent, - image = $image.src, - type = ($type[0] == 'movie'? 'movie': 'show'), - TMDbID = +$type[1]; - - let Db = await getIDs({ type, title, year, TMDbID, APIType: type, APIID: TMDbID }), - IMDbID = Db.imdb, - TVDbID = +Db.tvdb; - - title = title || Db.title; - year = +year || Db.year; - - let savename = title.toLowerCase(), - cached = await load(`${savename}.tmdb`); - - if(!cached) { - save(`${savename} (${year}).tmdb`, { type, title, year, imdb: IMDbID, tmdb: TMDbID, tvdb: TVDbID }); - save(`${savename}.tmdb`, +year); - terminal.log(`Saved as "${savename} (${year}).tmdb"`); - } - - return { type, title, year, image, IMDbID, TMDbID, TVDbID }; -} - -function initList() { - let $listItems = document.querySelectorAll('.item.card'), - button = renderPlexButton(true), /* see if a button was already created */ - options = [], length = $listItems.length - 1; - - if (!button) - return /* Fatal Error: Fail Silently */; - - $listItems.forEach(async(element, index, array) => { - let option = await addInListItem(element); - - if(option) - options.push(option); - - if(index == length) - setTimeout(() => { - if (!options.length) - new Notification('error', 'Failed to process list'); - else - squabblePlex(options, button); - }, 50); - }); -} - -parseOptions().then(() => { - init(); -}); +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'tmdb' }))(); diff --git a/src/sites/trakt/index.js b/src/sites/trakt/index.js index fa16184..4e035f8 100644 --- a/src/sites/trakt/index.js +++ b/src/sites/trakt/index.js @@ -1,193 +1,2 @@ -/* global wait, modifyPlexButton, parseOptions, findPlexMedia */ -let $$ = (element, all = false) => (element = document.querySelectorAll(element)).length > 1 && all? element: element[0]; - -function isMoviePage() { - return !isDash() && window.location.pathname.startsWith('/movies/'); -} - -function isShowPage() { - return !isDash() && window.location.pathname.startsWith('/shows/'); -} - -function isDash() { - return /^\/(dashboard|calendars|people|search|(?:movies|shows)\/(?:trending|popular|watched|collected|anticipated|boxoffice)|$)/i.test(window.location.pathname); -} - -function getIMDbID() { - let $link = $$( - // HTTPS and HTTP - '[href*="imdb.com/title/tt"]' - ); - - if ($link) - return $link.href.replace(/^.*?imdb\.com\/.+\b(tt\d+)\b/, '$1'); -} - -function getTVDbID() { - let $link = $$( - // HTTPS and HTTP - '[href*="thetvdb.com/"]' - ); - - if ($link) - return $link.href.replace(/^.*?thetvdb.com\/.+(?:(?:series\/?(?:\?id=)?)(\d+)\b).*?$/, '$1'); -} - -function getTMDbID() { - let $link = $$( - // HTTPS and HTTP - '[href*="themoviedb.org/"]' - ); - - if ($link) - return $link.href.replace(/^.*?themoviedb.org\/(?:movie|tv|shows?|series)\/(\d+).*?$/, '$1'); -} - -function init() { - if (isMoviePage() || isShowPage() || isDash()) { - wait( - () => ($$('#info-wrapper ul.external, .format-date') || document.readyState == 'complete'), - () => (isDash()? initDash(): initPlexThingy(isMoviePage() ? 'movie' : 'show')) - ); - } -} - -async function initPlexThingy(type) { - let button = renderPlexButton(); - - if (!button) - return /* Fatal Error: Fail Silently */; - - let $title = $$('.mobile-title'), - $year = $$('.mobile-title .year'), - $image = $$('.poster img.real[alt="poster" i]'); - - if (!$title || !$year) - return modifyPlexButton(button, 'error', `Could not extract ${ !$title? 'title': 'year' } from Trakt`); - - let title = $title.textContent.replace(/(.+)(\d{4}).*?$/, '$1').replace(/\s*\:\s*Season.*$/i, '').trim(), - year = +(RegExp.$2 || $year.textContent).trim(), - image = ($image || {}).src, - IMDbID = getIMDbID(), - TMDbID = getTMDbID(), - TVDbID = getTVDbID(); - - if(!IMDbID && !TMDbID && !TVDbID) { - let Db = await getIDs({ title, year, type, IMDbID, TMDbID, TVDbID }); - - IMDbID = IMDbID || Db.imdb, - TMDbID = TMDbID || Db.tmdb, - TVDbID = TVDbID || Db.tvdb; - title = Db.title; - year = Db.year; - } - - let o = (type == 'movie')? { im: IMDbID, tm: +TMDbID }: { im: IMDbID, tm: +TMDbID, tv: +TVDbID }; - - /* use Trakt as a caching service when applicable */ - /* yes, Trakt asks not to scrape their site, and we're not saving this to a server, so I'm gonna say OK */ - let savename = title.toLowerCase(), - cached = {}; - - if(type == 'movie') { - cached.tmdb = await load(`${savename}.tmdb`); - cached.imdb = await load(`${savename}.imdb`); - - if(!cached.tmdb) { - save(`${title} (${year}).tmdb`, { title, year, imdb: o.im, tmdb: o.tm }); - save(`${title}.tmdb`, year); - } - - if(!cached.imdb) { - save(`${title} (${year}).imdb`, { title, year, imdb: o.im }); - save(`${title}.imdb`, year); - } - } else { - cached.tvdb = await load(`${savename}.tvdb`); - cached.tmdb = await load(`${savename}.tmdb`); - cached.imdb = await load(`${savename}.imdb`); - - if(!cached.tvdb) { - save(`${title} (${year}).tvdb`, { title, year, tvdb: o.tv, tmdb: o.tm, imdb: o.im }); - save(`${title}.tvdb`, year); - } - - if(!cached.tmdb) { - save(`${title} (${year}).tmdb`, { title, year, imdb: o.im, tmdb: o.tm }); - save(`${title}.tmdb`, year); - } - - if(!cached.imdb) { - save(`${title} (${year}).imdb`, { title, year, imdb: o.im }); - save(`${title}.imdb`, year); - } - } - - findPlexMedia({ type, title, year, image, button, IMDbID, TMDbID, TVDbID }); -} - -async function initDash() { - let buttons = $$(".btn-watch-now, .quick-icons .watch-now", true); - - buttons.forEach((element, index, array) => { - element.preclick = element.preclick || element.onclick || (() => {}); - - element.onclick = async(event, rerun) => { - event.path.filter((v, i, a) => !!~[].slice.call(buttons).indexOf(v)).forEach((e, i, a) => e.preclick(event)); - - let ready = /^[^]+$/.test($$('#watch-now-content').innerText); - - if(!ready || !rerun) - return setTimeout( () => element.onclick(event, true), 5 ); - - let title = $$("#watch-now-content h3").innerText.replace(/^\s*where\s+to\s+watch\s*/i, ''), - image = $$('.poster img.real[alt="poster" i]'), - type = 'show', - year = YEAR, - button = $$(".w2p-channel"); - - if(title == '') - title = $$("#watch-now-content h1").innerText.replace(/^\s*(.+)\s+(\d+)\s*$/, '$1'), - year = RegExp.$2, - type = 'movie'; - - title = title.toCaps(); - - if(!button) { - $$("#watch-now-content .streaming-links").innerHTML += -` -
ondemand
- -`; - wait(() => button = $$(".w2p-channel"), () => {}); - } - - let Db = await getIDs({ title, year, type }), - IMDbID = Db.imdb, - TMDbID = +Db.tmdb, - TVDbID = +Db.tvdb; - - title = Db.title; - year = Db.year; - - findPlexMedia({ type, title, year, button, IMDbID, TMDbID, TVDbID, txt: 'title', hov: 'null' }); - }; - }); -} - -parseOptions().then(() => { - window.addEventListener('popstate', init); - window.addEventListener('pushstate-changed', init); - init(); -}); - -window.onlocationchange = (event) => { - init(); -}; +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'trakt' }))(); diff --git a/src/sites/tubi/index.css b/src/sites/tubi/index.css new file mode 100644 index 0000000..e69de29 diff --git a/src/sites/tubi/index.js b/src/sites/tubi/index.js new file mode 100644 index 0000000..fbe9b35 --- /dev/null +++ b/src/sites/tubi/index.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'tubi' }))(); diff --git a/src/sites/tvdb/index.js b/src/sites/tvdb/index.js index 447b099..8486015 100644 --- a/src/sites/tvdb/index.js +++ b/src/sites/tvdb/index.js @@ -1,75 +1,2 @@ -/* global findPlexMedia, parseOptions, modifyPlexButton */ -function isShowPage() { - // An example movie page: /series/gravity-falls - return window.location.pathname.startsWith('/series/'); -} - -function isShowPageReady() { - return !!document.querySelector('#series_basic_info'); -} - -function init() { - if (isShowPage()) - if (isShowPageReady()) - initPlexThingy(); - else - // This almost never happens, but sometimes the page is too slow so we need to wait a bit. - setTimeout(init, 1000); -} - -parseOptions().then(() => { - window.addEventListener('popstate', init); - window.addEventListener('pushstate-changed', init); - init(); -}); - -async function initPlexThingy() { - let button = renderPlexButton(); - - if (!button) - return /* Fatal Error: Fail Silently */; - - let $title = document.querySelector('#series_title'), - $image = document.querySelector('img[src*="/posters/"]'); - - if (!$title) - return modifyPlexButton( - button, - 'error', - `Could not extract title from TheTVDb` - ), - null; - - let title = $title.innerText.trim(), - year, - image = ($image || {}).src, - d = '', o = {}, - Db = document.querySelector('#series_basic_info') - .textContent - .replace(/^\s+|\s+$/g, '') - .replace(/^\s+$/gm, d) - .replace(/^\s+(\S)/gm, '$1') - .split(RegExp(`\\n*${d}\\n*`)) - .forEach(value => { - value = value.split(/\n+/, 2); - - let n = value[0], v = value[1]; - - n = n.replace(/^([\w\s]+).*$/, '$1').replace(/\s+/g, '_').toLowerCase(); - - o[n] = /,/.test(v)? v.split(/\s*,\s*/): v; - }); - - year = +(((o.first_aired || YEAR) + "").slice(0, 4)); - - let savename = title.toLowerCase(), - cached = await load(`${savename}.tvdb`); - - if(!cached) { - save(`${savename} (${year}).tvdb`, { title, year, tvdb: +o.thetvdb, imdb: o.imdb }); - save(`${savename}.tvdb`, +year); - terminal.log(`Saved as "${savename} (${year}).tvdb"`); - } - - findPlexMedia({ title, year, image, button, type: 'show', IMDbID: o.imdb, TVDbID: +o.thetvdb }); -} +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'tvdb' }))(); diff --git a/src/sites/tvmaze/index.js b/src/sites/tvmaze/index.js index 084580d..d704d5c 100644 --- a/src/sites/tvmaze/index.js +++ b/src/sites/tvmaze/index.js @@ -1,57 +1,2 @@ -/* global findPlexMedia, parseOptions, modifyPlexButton */ -function isShowPage() { - // An example movie page: /shows/2757/colony - return window.location.pathname.startsWith('/shows/'); -} - -function isShowPageReady() { - return !!document.querySelector('#general-info-panel .rateit'); -} - -async function init() { - if (isShowPage()) - if (isShowPageReady()) - await initPlexThingy(); - else - // This almost never happens, but sometimes the page is too slow so we need to wait a bit. - setTimeout(init, 1000); -} - -parseOptions().then(async() => { - window.addEventListener('popstate', init); - window.addEventListener('pushstate-changed', init); - await init(); -}); - -async function initPlexThingy() { - let button = renderPlexButton(); - - if (!button) - return /* Fatal Error: Fail Silently */; - - let $title = document.querySelector('header.columns > h1'), - $date = document.querySelector('#year'), - $image = document.querySelector('figure img'), - $apid = window.location.pathname.replace(/\/shows\/(\d+).*/, '$1'); - - if (!$title || !$date) - return modifyPlexButton( - button, - 'error', - `Could not extract ${ !$title? 'title': 'year' } from TV Maze` - ), - null; - - let title = $title.innerText.trim(), - year = $date.innerText.replace(/\((\d+).+\)/, '$1'), - image = ($image || {}).src, - Db = await getIDs({ title, year, type: 'tv', APIID: $apid }), - IMDbID = Db.imdb, - TMDbID = Db.tmdb, - TVDbID = Db.tvdb; - - title = Db.title; - year = Db.year; - - findPlexMedia({ title, year, button, type: 'tv', IMDbID, TMDbID, TVDbID }); -} +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'tvmaze' }))(); diff --git a/src/sites/verizon/index.js b/src/sites/verizon/index.js index a922c25..b7294b1 100644 --- a/src/sites/verizon/index.js +++ b/src/sites/verizon/index.js @@ -1,70 +1,2 @@ -/* global wait, modifyPlexButton, parseOptions, findPlexMedia */ -function isMoviePage() { - return /\bmovies?\b/i.test(window.location.pathname); -} - -function isShowPage() { - return /\bseries\b/i.test(window.location.pathname); -} - -function isOnDemand() { - return /ondemand/i.test(window.location.pathname); -} - -function init() { - if (isMoviePage() || isShowPage()) { - wait( - () => document.querySelector('.container .btn-with-play, .moredetails, .more-like'), - () => initPlexThingy(isMoviePage() ? 'movie' : 'tv') - ); - } -} - -async function initPlexThingy(type) { - let button = renderPlexButton(); - - if (!button) - return /* Fatal Error: Fail Silently */; - - let $title, $year, $image = document.querySelector('.cover img'); - - if(isOnDemand()) { - if(isMoviePage()) { - $title = document.querySelector('.detail *'); - $year = document.querySelector('.rating *'); - } else { - $title = {textContent: window.location.pathname.replace(/\/ondemand\/tvshows?\/([^\/]+?)\/.*/i)}; - $year = document.querySelector('#showDetails > * > *:nth-child(4) *:last-child'); - - $title.textContent = decodeURL($title.textContent).toCpas(); - } - } else { - $title = document.querySelector('.copy > .title'); - $year = (type === 'movie')? - document.querySelector('.copy > .details'): - document.querySelector('.summary ~ .title ~ *'); - } - - if (!$title || !$year) - return modifyPlexButton(button, 'error', `Could not extract ${ !$title? 'title': 'year' } from Verizon`); - - let title = $title.textContent.trim(), - year = $year.textContent.slice(0, 4).trim(), - image = ($image || {}).src; - - let Db = await getIDs({ title, year, type }), - IMDbID = Db.imdb, - TMDbID = Db.tmdb, - TVDbID = Db.tvdb; - - title = Db.title; - year = Db.year; - - findPlexMedia({ type, title, year, image, button, IMDbID, TMDbID, TVDbID }); -} - -parseOptions().then(() => { - window.addEventListener('popstate', init); - window.addEventListener('pushstate-changed', init); - init(); -}); +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'verizon' }))(); diff --git a/src/sites/vrv/index.js b/src/sites/vrv/index.js index 0901970..c4a2052 100644 --- a/src/sites/vrv/index.js +++ b/src/sites/vrv/index.js @@ -1,119 +1,2 @@ -/* global findPlexMedia, parseOptions, modifyPlexButton */ -function isShow() { - // An example movie page: /series/GR75MN7ZY/Deep-Space-69-Unrated - return /^\/(?:series)\//.test(window.location.pathname) || (/^\/(?:watch)\//.test(window.location.pathname) && document.querySelector('.content .series')); -} - -function isMovie() { - return /^\/(?:watch)\//.test(window.location.pathname) && !document.querySelector('.content .series'); -} - -function isPageReady() { - let img = document.querySelector('.h-thumbnail > img'), - pre = document.querySelector('#content .content .card'); - return isList()? pre && pre.textContent: img && img.src; -} - -function isList() { - return /\/(watchlist)\b/i.test(window.location.pathname); -} - -function init() { - if (isPageReady()) { - if (isShow()) - initPlexThingy('show'); - else if (isMovie()) - initPlexThingy('movie'); - else if(isList()) - initList(); - } else { - // This almost never happens, but sometimes the page is too slow so we need to wait a bit. - setTimeout(init, 1000); - } -} - -parseOptions().then(() => { - window.addEventListener('popstate', init); - window.addEventListener('pushstate-changed', init); - (window.onlocationchange = init)(); -}); - -async function initPlexThingy(type) { - let button = renderPlexButton(); - - if (!button) - return /* Fatal Error: Fail Silently */; - - let $title = document.querySelector('.series, [class*="video"] .title, [class*="series"] .title'), - $year = document.querySelector('.additional-information-item'), - $image = document.querySelector('[class*="poster"][class*="wrapper"] img'); - - if (!$title) - return modifyPlexButton( - button, - 'error', - `Could not extract title from VRV` - ), - null; - - let title = $title.innerText.replace(/(unrated|mature|tv-?\d{1,2})\s*$/i, '').trim(), - year = $year? $year.textContent.replace(/.+(\d{4}).*/, '$1').trim(): 0, - image = ($image || {}).src, - Db = await getIDs({ type, title, year }), - IMDbID = Db.imdb, - TMDbID = Db.tmdb, - TVDbID = Db.tvdb; - - title = title || Db.title; - year = year || Db.year; - - findPlexMedia({ type, title, year, image, button, IMDbID, TMDbID, TVDbID }); -} - -async function addInListItem(element) { - let $title = element.querySelector('.info > *'), - $image = element.querySelector('.poster-image img'), - $type = element.querySelector('.info [class*="series"], .info [class*="movie"]'); - - if (!$title || !$type) - return; - - let title = $title.textContent.trim(), - image = $image.src, - type = $type.getAttribute('class').replace(/[^]*(movie|series)[^]*/, '$1'), - year; - - let Db = await getIDs({ type, title }), - IMDbID = Db.imdb, - TMDbID = Db.tmdb, - TVDbID = +Db.tvdb; - - title = title || Db.title; - year = Db.year; - - return { type, title, year, image, IMDbID, TMDbID, TVDbID }; -} - -function initList() { - let $listItems = document.querySelectorAll('#content .content .card'), - button = renderPlexButton(), - options = [], length = $listItems.length - 1; - - if (!button) - return /* Fatal Error: Fail Silently */; - - $listItems.forEach(async(element, index, array) => { - let option = await addInListItem(element); - - if(option) - options.push(option); - - if(index == length) - setTimeout(() => { - if (!options.length) - new Notification('error', 'Failed to process list'); - else - squabblePlex(options, button); - }, 50); - }); -} +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'vrv' }))(); diff --git a/src/sites/vudu/index.js b/src/sites/vudu/index.js index bf346af..303f5ba 100644 --- a/src/sites/vudu/index.js +++ b/src/sites/vudu/index.js @@ -1,59 +1,2 @@ -/* global findPlexMedia, parseOptions, modifyPlexButton */ -function isMovie() { - return !isShow(); -} - -function isShow() { - return /(?:Season-\d+\/\d+)$/i.test(window.location.pathname); -} - -function isPageReady() { - return !!document.querySelector('img[src*="poster" i]'); -} - -async function init() { - if (isPageReady()) - await initPlexThingy(isMovie()? 'movie': isShow()? 'tv': null); - else - // This almost never happens, but sometimes the page is too slow so we need to wait a bit. - setTimeout(init, 1000); -} - -async function initPlexThingy(type) { - let button = renderPlexButton(); - - if (!button || !type) - return /* Fatal Error: Fail Silently */; - - let $title = document.querySelector('.head-big'), - $date = document.querySelector('.container .row:first-child .row ~ * > .row span'), - $image = document.querySelector('img[src*="poster" i]'); - - if (!$title) - return modifyPlexButton( - button, - 'error', - `Could not extract title from Vudu` - ); - - let title = $title.textContent.replace(/\((\d{4})\)/, '').trim(), - year = $date? $date.textContent.split(/\s*\|\s*/): RegExp.$1, - image = ($image || {}).src; - - year = +year[year.length - 1].slice(0, 4); - year |= 0; - - let Db = await getIDs({ title, year, type }), - IMDbID = Db.imdb, - TMDbID = Db.tmdb, - TVDbID = Db.tvdb; - - title = Db.title; - year = Db.year; - - findPlexMedia({ type, title, year, image, button, IMDbID, TMDbID, TVDbID }); -} - -if (isMovie() || isShow()) { - parseOptions().then(async() => await (window.onlocationchange = init)()); -} +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'vudu' }))(); diff --git a/src/sites/vumoo/index.css b/src/sites/vumoo/index.css new file mode 100644 index 0000000..e69de29 diff --git a/src/sites/vumoo/index.js b/src/sites/vumoo/index.js new file mode 100644 index 0000000..2793ee0 --- /dev/null +++ b/src/sites/vumoo/index.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'vumoo' }))(); diff --git a/src/sites/webtoplex/index.css b/src/sites/webtoplex/index.css new file mode 100644 index 0000000..e69de29 diff --git a/src/sites/webtoplex/index.js b/src/sites/webtoplex/index.js new file mode 100644 index 0000000..291f462 --- /dev/null +++ b/src/sites/webtoplex/index.js @@ -0,0 +1,2 @@ +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'webtoplex' }))(); diff --git a/src/sites/youtube/index.js b/src/sites/youtube/index.js index 0e215b7..7512c79 100644 --- a/src/sites/youtube/index.js +++ b/src/sites/youtube/index.js @@ -1,54 +1,2 @@ -let $$ = selector => document.querySelector(selector); - -function isMovie(owner) { - return /\byoutube movies\b/i.test(owner); -} - -function isShow() { - let __title__ = $$('.super-title'); - - return __title__ && /\bs\d+\b.+\be\d+\b/i.test(__title__.textContent); -} - -async function init() { - let owner = $$('#owner-container').textContent.replace(/^\s+|\s+$/g, ''); - - $$('.more-button').click(); // show the year and other information, fails otherwise - - if(isMovie(owner) || isShow()) - await initPlexThingy(isMovie(owner)? 'movie': isShow()? 'show': null); - - $$('.less-button').click(); // close the meta-information -} - -async function initPlexThingy(type) { - let button = renderPlexButton(); - if(!button || !type) - return /* Fail silently */; - - let $title = (type == 'movie'? $$('.title'): $$('#owner-container')), - $date = $$('#content ytd-expander'); - - if(!$title || !$date) - return modifyPlexButton(button, 'error', 'Could not extract title or year from YouTube'); - - let title = $title.textContent.trim(), - year = +$date.textContent.replace(/[^]*(?:release|air) date\s+(?:(?:\d+\/\d+\/)?(\d{2,4}))[^]*/i, ($0, $1, $$, $_) => +$1 < 1000? 2000 + +$1: $1); - - let Db = await getIDs({ title, year, type }), - IMDbID = Db.imdb, - TMDbID = Db.tmdb, - TVDbID = Db.tvdb; - - title = Db.title; - year = Db.year; - - findPlexMedia({ type, title, year, button, IMDbID, TMDbID, TVDbID }); -} - -parseOptions() - .then(() => { - window.addEventListener('popstate', init); - window.addEventListener('pushstate-changed', init); - wait(() => $$('#owner-container'), init) - }); +/* global Update(type:string, details:object) */ +(init = () => Update('SCRIPT', { script: 'youtube' }))(); diff --git a/src/utils.js b/src/utils.js index a276266..48456eb 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,1715 +1,2585 @@ /* eslint-disable no-unused-vars */ -/* global config */ -function wait(on, then) { - if (on()) - then && then(); - else - setTimeout(() => wait(on, then), 50); -} +/* global configuration, init, Update, "Helpers" */ + +let configuration, init, Update, browser; + +(async date => { + + // default date items + let YEAR = date.getFullYear(), + MONTH = date.getMonth() + 1, + DATE = date.getDate(), + // Notification items + NOTIFIED = false, + RUNNING = false, + // Other items + /* Items that the user has already asked for */ + CAUGHT; + + // simple helpers + let extURL = url => (browser? browser.runtime: chrome.extension).getURL(url), + $ = (selector, container) => queryBy(selector, container), + // DO NOT EXPOSE + __CONFIG__, ALLOWED, PERMISS; + + let IMG_URL = { + 'nil': extURL('img/null.png'), + 'icon_16': extURL('img/16.png'), + 'icon_48': extURL('img/48.png'), + 'hide_icon_16': extURL('img/hide.16.png'), + 'hide_icon_48': extURL('img/hide.48.png'), + 'show_icon_16': extURL('img/show.16.png'), + 'show_icon_48': extURL('img/show.48.png'), + 'close_icon_16': extURL('img/close.16.png'), + 'close_icon_48': extURL('img/close.48.png'), + 'icon_white_16': extURL('img/_16.png'), + 'icon_white_48': extURL('img/_48.png'), + 'plexit_icon_16': extURL('img/plexit.16.png'), + 'plexit_icon_48': extURL('img/plexit.48.png'), + 'reload_icon_16': extURL('img/reload.16.png'), + 'reload_icon_48': extURL('img/reload.48.png'), + 'icon_outline_16': extURL('img/o16.png'), + 'icon_outline_48': extURL('img/o48.png'), + 'noise_background': extURL('img/noise.png'), + 'settings_icon_16': extURL('img/settings.16.png'), + 'settings_icon_48': extURL('img/settings.48.png'), + }; + + // the storage - priority to sync + const UTILS_STORAGE = chrome.storage.sync || chrome.storage.local; + + async function load(name = '') { + if(!name) + return /* invalid name */; + + name = '~/cache/' + (name.toLowerCase().replace(/\s+/g, '_')); + + return new Promise((resolve, reject) => { + function LOAD(DISK) { + let data = JSON.parse(DISK[name] || null); + + return resolve(data); + } -let NO_DEBUGGER = false; - -let date = (new Date), - terminal = - NO_DEBUGGER? - { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m }: - console; - -let YEAR = date.getFullYear(), - MONTH = date.getMonth() + 1, - DATE = date.getDate(); - -let getURL = url => chrome.extension.getURL(url); - -let IMG_URL = { - 'i16': getURL('img/16.png'), - 'i48': getURL('img/48.png'), - '_16': getURL('img/_16.png'), - '_48': getURL('img/_48.png'), - 'o16': getURL('img/o16.png'), - 'o48': getURL('img/o48.png'), - 'h16': getURL('img/hide.16.png'), - 'h48': getURL('img/hide.48.png'), - 'j16': getURL('img/show.16.png'), - 'j48': getURL('img/show.48.png'), - 'p16': getURL('img/plexit.16.png'), - 'p48': getURL('img/plexit.48.png'), - 'r16': getURL('img/reload.16.png'), - 'r48': getURL('img/reload.48.png'), - 'x16': getURL('img/close.16.png'), - 'x48': getURL('img/close.48.png'), - 's16': getURL('img/settings.16.png'), - 's48': getURL('img/settings.48.png'), - 'noi': getURL('img/noise.png'), - 'nil': getURL('img/null.png'), -}; + UTILS_STORAGE.get(null, DISK => { + if(chrome.runtime.lastError) + chrome.storage.local.get(null, LOAD); + else + LOAD(DISK); + }); + }); + } -// the custom "on location change" event -let locationchangecallbacks = []; + async function save(name = '', data) { + if(!name) + return /* invalid name */; -function watchlocationchange(subject) { - watchlocationchange[subject] = watchlocationchange[subject] || location[subject]; + name = '~/cache/' + (name.toLowerCase().replace(/\s+/g, '_')); + data = JSON.stringify(data); - if (watchlocationchange[subject] != location[subject]) { - watchlocationchange[subject] = location[subject]; + // erase entries after 400-500 have been made + UTILS_STORAGE.get(null, items => { + let array = [], bytes = 0; - for(let index = 0, length = locationchangecallbacks.length, callback; index < length; index++) { - callback = locationchangecallbacks[index]; + for(let item in items) { + let object = items[item]; - if(callback && typeof callback == 'function') - callback(new Event('locationchange', { bubbles: true })); - } - } -} + array.push(item); + bytes += (typeof object == 'string'? object.length * 8: typeof object == 'boolean'? 8: JSON.stringify(object).length * 8)|0; + } -Object.defineProperty(window, 'onlocationchange', { - set: callback => locationchangecallbacks.push(callback) -}); + if((UTILS_STORAGE.MAX_ITEMS && array.length >= UTILS_STORAGE.MAX_ITEMS) || bytes >= UTILS_STORAGE.QUOTA_BYTES) { + UTILS_TERMINAL.warn('Exceeded quota. Erasing cache...'); -setInterval(() => watchlocationchange('pathname'), 1000); // at least 1s is needed to properly fire the event ._. + for(let item in items) + if(/^~\/cache\/(?!get|has)/i.test(item)) + UTILS_STORAGE.remove(item); -// the storage -const storage = chrome.storage.sync || chrome.storage.local; + UTILS_TERMINAL.log('Cache erased'); + } + }); + + await UTILS_STORAGE.set({[name]: data}, () => data); + + return name; + } + + async function remove(name) { + if(!name) + return /* invalid name */; + + return await UTILS_STORAGE.remove(['~/cache/' + (name.toLowerCase().replace(/\s+/g, '_'))]); + } + + function encode(data) { + if(/^[\u0000-\u00ff]+$/.test(data)) + return btoa(data); + else + return data; + } + + function decode(data) { + if(/^[a-z\d\+\/\=]+$/i.test(data)) + return atob(data); + else + return data; + } + + /* Notifications */ + // create and/or queue a notification + // state = "warning" - red + // state = "error" + // state = "update" - blue + // state = "info" - grey + // anything else for state will show as orange + class Notification { + constructor(state, text, timeout = 7000, callback = () => {}, requiresClick = true) { + let queue = (Notification.queue = Notification.queue || { list: [] }), + last = queue.list[queue.list.length - 1] || document.queryBy('.web-to-plex-notification').first; + + if(!__CONFIG__) { + Options(); + + throw 'No configuration saved...'; + } -async function load(name = '') { - if(!name) return; + if(((state == 'error' || state == 'warning') && __CONFIG__.NotifyNewOnly && /\balready\s+(exists?|(been\s+)?added)\b/.test(text)) || (__CONFIG__.NotifyOnlyOnce && NOTIFIED && state === 'info')) + return /* Don't match /.../i as to not match item titles */; + NOTIFIED = true; - name = 'Cache-Data/' + btoa(name.toLowerCase().replace(/\s+/g, '')); + if(last && !last.done) + return (last => setTimeout(() => new Notification(state, text, timeout, callback, requiresClick), +(new Date) - last.start))(last); - return new Promise((resolve, reject) => { - function LOAD(DISK) { - let data = JSON.parse(DISK[name] || null); + let element = furnish(`div.web-to-plex-notification.${state}`, { + onmouseup: event => { + let notification = Notification.queue[event.target.id], + element = notification.element; - return resolve(data); - } + notification.done = true; + Notification.queue.list.splice(notification.index, 1); + clearTimeout(notification.job); + element.remove(); - storage.get(null, DISK => { - if (chrome.runtime.lastError) - chrome.storage.local.get(null, LOAD); - else - LOAD(DISK); - }); - }); -} + let removed = delete Notification.queue[notification.id]; -async function save(name = '', data) { - if(!name) return; + return (event.requiresClick)? null: notification.callback(removed); + } + }, text); - name = 'Cache-Data/' + btoa(name.toLowerCase().replace(/\s+/g, '')); - data = JSON.stringify(data); + queue[element.id = +(new Date)] = { + start: +element.id, + stop: +element.id + timeout, + span: +timeout, + done: false, + index: queue.list.length, + job: setTimeout(() => element.onmouseup({ target: element, requiresClick }), timeout), + id: +element.id, + callback, element + }; + queue.list.push(queue[element.id]); - await storage.set({[name]: data}, () => data); + document.body.appendChild(element); - return name; -} + return queue[element.id]; + } + } + + class Prompt { + constructor(prompt_type, options, callback = () => {}, container = document.body) { + let prompt, remove, + array = (options instanceof Array? options: [].slice.call(options)), + data = [...array], + profiles = { + movie: JSON.parse( + __CONFIG__.usingRadarr? + __CONFIG__.radarrQualities: + __CONFIG__.usingWatcher? + __CONFIG__.watcherQualities: + '[]' + ), + show: JSON.parse( + __CONFIG__.usingSonarr? + __CONFIG__.sonarrQualities: + __CONFIG__.usingMedusa? + __CONFIG__.medusaQualities: + __CONFIG__.usingSickBeard? + __CONFIG__.sickBeardQualities: + '[]' + ) + }, + locations = { + movie: JSON.parse( + __CONFIG__.usingRadarr? + __CONFIG__.radarrStoragePaths: + __CONFIG__.usingWatcher? + __CONFIG__.watcherStoragePaths: + '[]' + ), + show: JSON.parse( + __CONFIG__.usingSonarr? + __CONFIG__.sonarrStoragePaths: + __CONFIG__.usingMedusa? + __CONFIG__.medusaStoragePaths: + __CONFIG__.usingSickBeard? + __CONFIG__.sickBeardStoragePaths: + '[]' + ) + }, + defaults = { + movie: ( + __CONFIG__.usingRadarr? + { quality: __CONFIG__.__radarrQuality, location: __CONFIG__.__radarrStoragePath }: + {} + ), + show: ( + __CONFIG__.usingSonarr? + { quality: __CONFIG__.__sonarrQuality, location: __CONFIG__.__sonarrStoragePath }: + __CONFIG__.usingMedusa? + { quality: __CONFIG__.__medusaQuality, location: __CONFIG__.__medusaStoragePath }: + __CONFIG__.usingSickBeard? + { quality: __CONFIG__.__sickBeardQuality, location: __CONFIG__.__sickBeardStoragePath }: + {} + ) + }; + + switch(prompt_type) { + /* Allows the user to add and remove items from a list */ + case 'prompt': + case 'input': + remove = element => { + let prompter = $('.web-to-plex-prompt').first, + header = $('.web-to-plex-prompt-header').first, + counter = $('.web-to-plex-prompt-options').first; + + if(element === true) + return prompter.remove(); + else + element.remove(); + + data.splice(+element.value, 1, null); + header.innerText = 'Approve ' + counter.children.length + (counter.children.length == 1?' item': ' items'); + }; + + prompt = furnish('div.web-to-plex-prompt', {}, + furnish('div.web-to-plex-prompt-body', {}, + // The prompt's title + furnish('h1.web-to-plex-prompt-header', {}, 'Approve ' + array.length + (array.length == 1? ' item': ' items')), + + // The prompt's items + furnish('div.web-to-plex-prompt-options', {}, + ...(ITEMS => { + let elements = []; + + for(let index = 0, length = ITEMS.length, ITEM, P_QUA, P_LOC; index < length; index++) { + ITEM = ITEMS[index]; + + elements.push( + furnish('li.web-to-plex-prompt-option.mutable', { value: index, innerHTML: `

${ index + 1 } \u00b7 ${ ITEM.title }${ ITEM.year? ` (${ ITEM.year })`: '' } \u2014 ${ ITEM.type }

` }, + furnish('button.remove', { title: `Remove "${ ITEM.title }"`, onmouseup: event => { remove(event.target.parentElement); event.target.remove() } }), + ( + __CONFIG__.PromptQuality? + P_QUA = furnish('select.quality', { index, onchange: event => data[event.target.getAttribute('index')].quality = event.target.value }, ...profiles[/(movie|film|cinema)/i.test(ITEM.type)?'movie':'show'].map(Q => furnish('option', { value: Q.id }, Q.name))): + '' + ),( + __CONFIG__.PromptLocation? + P_LOC = furnish('select.location', { index, onchange: event => data[event.target.getAttribute('index')].location = event.target.value }, ...locations[/(movie|film|cinema)/i.test(ITEM.type)?'movie':'show'].map(Q => furnish('option', { value: Q.id }, Q.path))): + '' + ) + ) + ); + + if(P_QUA) P_QUA.value = defaults[ITEM.type].quality; + if(P_LOC) P_LOC.value = defaults[ITEM.type].location; + + P_QUA = P_LOC = null; + } + + return elements + })(array) + ), + + // The engagers + furnish('div.web-to-plex-prompt-footer', {}, + furnish('input.web-to-plex-prompt-input[type=text]', { placeholder: 'Add an item (enter to add): Title (Year) Type / ID Type', title: 'Solo: A Star Wars Story (2018) movie / tt3778644 m', onkeydown: async event => { + if(event.keyCode == 13) { + let title, year, type, self = event.target, R = RegExp, + movie = /^(m(?:ovies?)?|f(?:ilms?)?|c(?:inemas?)?)/i, + Db, IMDbID, TMDbID, TVDbID, value = self.value; + + self.setAttribute('disabled', self.disabled = true); + self.value = `Searching for "${ value }"...`; + data = data.filter(value => value !== null && value !== undefined); + + if(/^\s*((?:tt)?\d+)(?:\s+(\w+)|\s*)?$/i.test(value)) { + let APIID = R.$1, + type = R.$2 || (data.length? data[0].type: 'movie'), + APIType = movie.test(type)? /^tt/i.test(APIID)? 'imdb': 'tmdb': 'tvdb'; + + type = movie.test(type)? 'movie': 'show'; + + Db = await Identify({ type, APIID, APIType }); + IMDbID = Db.imdb; + TMDbID = Db.tmdb; + TVDbID = Db.tvdb; + + title = Db.title; + year = Db.year; + } else if(/^([^]+)(\s*\(\d{2,4}\)\s*|\s+\d{2,4}\s+)([\w\s\-]+)$/.test(value)) { + title = R.$1; + year = R.$2 || YEAR + ''; + type = R.$3 || (data.length? data[0].type: 'movie'); + + year = +year.replace(/\D/g, '').replace(/^\d{2}$/, '20$&'); + type = movie.test(type)? 'movie': 'show'; + + Db = await Identify({ type, title, year }); + IMDbID = Db.imdb; + TMDbID = Db.tmdb; + TVDbID = Db.tvdb; + } + + event.preventDefault(); + if(type && title && !(/^(?:tt)?$/i.test(IMDbID || '') && /^0?$/.test(+TMDbID | 0) && /^0?$/.test(+TVDbID | 0))) { + remove(true); + new Prompt(prompt_type, [{ ...Db, type, IMDbID, TMDbID, TVDbID }, ...data], callback, container); + } else { + self.disabled = self.removeAttribute('disabled'); + self.value = value; + new Notification('error', `Couldn't find "${ value }"`); + } + } + } }), + furnish('button.web-to-plex-prompt-decline', { onmouseup: event => { remove(true); callback([]) }, title: 'Close' }, '\u2718'), + furnish('button.web-to-plex-prompt-accept', { onmouseup: event => { remove(true); new Prompt(prompt_type, options, callback, container) }, title: 'Reset' }, '\u21BA'), + furnish('button.web-to-plex-prompt-accept', { onmouseup: event => { remove(true); callback(data.filter(value => value !== null && value !== undefined)) }, title: 'Continue' }, '\u2714') + ) + ) + ); + break; + + /* Allows the user to remove predetermined items */ + case 'select': + remove = element => { + let prompter = $('.web-to-plex-prompt').first, + header = $('.web-to-plex-prompt-header').first, + counter = $('.web-to-plex-prompt-options').first; + + if(element === true) + return prompter.remove(); + else + element.remove(); + + data.splice(+element.value, 1, null); + header.innerText = 'Approve ' + counter.children.length + (counter.children.length == 1?' item': ' items'); + }; + + prompt = furnish('div.web-to-plex-prompt', {}, + furnish('div.web-to-plex-prompt-body', {}, + // The prompt's title + furnish('h1.web-to-plex-prompt-header', {}, 'Approve ' + array.length + (array.length == 1? ' item': ' items')), + + // The prompt's items + furnish('div.web-to-plex-prompt-options', {}, + ...(ITEMS => { + let elements = []; + + for(let index = 0, length = ITEMS.length, ITEM, P_QUA, P_LOC; index < length; index++) { + ITEM = ITEMS[index]; + + elements.push( + furnish('li.web-to-plex-prompt-option.mutable', { value: index, innerHTML: `

${ index + 1 } \u00b7 ${ ITEM.title }${ ITEM.year? ` (${ ITEM.year })`: '' } \u2014 ${ ITEM.type }

` }, + furnish('button.remove', { title: `Remove "${ ITEM.title }"`, onmouseup: event => { remove(event.target.parentElement); event.target.remove() } }), + ( + __CONFIG__.PromptQuality? + P_QUA = furnish('select.quality', { index, onchange: event => data[event.target.getAttribute('index')].quality = event.target.value }, ...profiles[/(movie|film|cinema)/i.test(ITEM.type)?'movie':'show'].map(Q => furnish('option', { value: Q.id }, Q.name))): + '' + ),( + __CONFIG__.PromptLocation? + P_LOC = furnish('select.location', { index, onchange: event => data[event.target.getAttribute('index')].location = event.target.value }, ...locations[/(movie|film|cinema)/i.test(ITEM.type)?'movie':'show'].map(Q => furnish('option', { value: Q.id }, Q.path))): + '' + ) + ) + ); + + if(P_QUA) P_QUA.value = defaults[ITEM.type].quality; + if(P_LOC) P_LOC.value = defaults[ITEM.type].location; + + P_QUA = P_LOC = null; + } + + return elements + })(array) + ), + + // The engagers + furnish('div.web-to-plex-prompt-footer', {}, + furnish('button.web-to-plex-prompt-decline', { onmouseup: event => { remove(true); callback([]) }, title: 'Close' }, '\u2718'), + furnish('button.web-to-plex-prompt-accept', { onmouseup: event => { remove(true); new Prompt(prompt_type, options, callback, container) }, title: 'Reset' }, '\u21BA'), + furnish('button.web-to-plex-prompt-accept', { onmouseup: event => { remove(true); callback(data.filter(value => value !== null && value !== undefined)) }, title: 'Continue' }, '\u2714') + ) + ) + ); + break; + + /* Allows the user to modify a single item (before being pushed) */ + case 'modify': + let { title, year, type, IMDbID, TMDbID, TVDbID } = options, + P_QUA, P_LOC; + + let i = IMDbID, + t = TMDbID, + v = TVDbID, + s = 'style="text-decoration: none !important; color: #cc7b19 !important; font-style: italic !important;" target="_blank"'; + + i = /^tt-?$/.test(i)? '': i; + t = /^0?$/.test(t)? '': t; + v = /^0?$/.test(v)? '': v; + + remove = element => { + let prompter = $('.web-to-plex-prompt').first, + header = $('.web-to-plex-prompt-header').first, + counter = $('.web-to-plex-prompt-options').first; + + if(element === true) + return prompter.remove(); + else + element.remove(); + }; + + type = /(movie|film|cinema)/i.test(type)?'movie':'show'; + + prompt = furnish('div.web-to-plex-prompt', {}, + furnish('div.web-to-plex-prompt-body', {}, + // The prompt's title + furnish('h1.web-to-plex-prompt-header', { innerHTML: `${ title }${ year? ` (${ year })`: '' } \u2014 ${ type }` }), + + // The prompt's items + furnish('div.web-to-plex-prompt-options', {}, + furnish('div.web-to-plex-prompt-option', { innerHTML: `${ i? `${i}`: '/' } \u2014 ${ t? `${t}`: '/' } \u2014 ${ v? `${v}`: '/' }` }), + ( + __CONFIG__.PromptQuality? + P_QUA = furnish('select.quality', { onchange: event => options.quality = event.target.value }, ...profiles[type].map(Q => furnish('option', { value: Q.id }, Q.name))): + '' + ), + furnish('br'), + ( + __CONFIG__.PromptLocation? + P_LOC = furnish('select.location', { onchange: event => options.location = event.target.value }, ...locations[type].map(Q => furnish('option', { value: Q.id }, Q.path))): + '' + ) + ), + + // The engagers + furnish('div.web-to-plex-prompt-footer', {}, + furnish('button.web-to-plex-prompt-decline', { onmouseup: event => { remove(true); callback([]) }, title: 'Close' }, '\u2718'), + furnish('button.web-to-plex-prompt-accept', { onmouseup: event => { remove(true); new Prompt(prompt_type, options, callback, container) }, title: 'Reset' }, '\u21BA'), + furnish('button.web-to-plex-prompt-accept', { onmouseup: event => { remove(true); callback(options) }, title: 'Continue' }, '\u2714') + ) + ) + ); + + if(P_QUA) P_QUA.value = defaults[type].quality; + if(P_LOC) P_LOC.value = defaults[type].location; + + P_QUA = P_LOC = null; + break; + + case 'permission': + let { permission, name, alias } = options; + let existing, permissions; + + /* Only one permission prompt allowed */ + if(!(existing = $('.web-to-plex-prompt[type=permission]')).empty) + return existing.first; + + UTILS_TERMINAL.log(`Asking for permission(s):`, options); + + remove = element => { + let prompter = $('.web-to-plex-prompt').first, + header = $('.web-to-plex-prompt-header').first, + counter = $('.web-to-plex-prompt-options').first; + + if(element === true) + return prompter.remove(); + else + element.remove(); + }; + + callback = (allowed, permissions) => { + save(`has/${ name }`, allowed); + save(`get/${ name }`, permissions); + + ALLOWED = allowed; + PERMISS = permissions; + + ParsedOptions(); + + return Update(`GRANT_PERMISSION`, { allowed, permissions }, true), + (init && !RUNNING? (init(), RUNNING = true): RUNNING = false); + }; + + prompt = furnish('div.web-to-plex-prompt', {}, + furnish('div.web-to-plex-prompt-body', {}, + // The prompt's title + furnish('h1.web-to-plex-prompt-header', { innerHTML: `${ alias || name } (${ location.host }) would like:` }), + + // The prompt's items + furnish('div.web-to-plex-prompt-options', {}, + ...((permissions = permission.split(/\s*,\s*/).filter(v=>v&&v.length)).map( + __permission => + furnish('div.web-to-plex-prompt-option', { innerHTML: `Access to your ${ __permission } information` }) + ) + ) + ), + + + // The engagers + furnish('div.web-to-plex-prompt-footer', {}, + furnish('button.web-to-plex-prompt-decline', { onmouseup: event => { remove(true); callback(false, {}) }, title: 'Deny' }, '\u2718'), + furnish('button.web-to-plex-prompt-accept', { onmouseup: event => { remove(true); callback(true, permissions) }, title: 'Allow' }, '\u2714') + ) + ) + ); + break; + + default: + return UTILS_TERMINAL.warn(`Unknown prompt type "${ prompt_type }"`); + break; + } -async function kill(name) { - return storage.remove(['Cache-Data/' + btoa(name.toLowerCase().replace(/\s+/g, ''))]); -} + prompt.setAttribute('type', prompt_type); -// create and/or queue a notification -// state = "error" - red -// state = "update" - blue -// state = "info" - grey -// anything else for state will show as orange -class Notification { - constructor(state, text, timeout = 7000, callback = () => {}, requiresClick = true) { - let queue = (Notification.queue = Notification.queue || { list: [] }), - last = queue.list[queue.list.length - 1]; - - if (last && last.done === false) - return (last => setTimeout(() => new Notification(state, text, timeout, callback, requiresClick), +(new Date) - last.start))(last); - - let element = document.furnish(`div.web-to-plex-notification.${state}`, { - onclick: event => { - let notification = Notification.queue[event.target.id], - element = notification.element; - - notification.done = true; - Notification.queue.list.splice(notification.index, 1); - clearTimeout(notification.job); - element.remove(); - - let removed = delete Notification.queue[notification.id]; - - return (event.requiresClick)? null: notification.callback(removed); - } - }, text); - - queue[element.id = +(new Date)] = { - start: +element.id, - stop: +element.id + timeout, - span: +timeout, - done: false, - index: queue.list.length, - job: setTimeout(() => element.onclick({ target: element, requiresClick }), timeout), - id: +element.id, - callback, element - }; - queue.list.push(queue[element.id]); - - document.body.appendChild(element); - - return queue[element.id]; - } -} + return container.append(prompt), prompt; + } + } -class Prompt { - constructor(prompt_type, options, callback = () => {}, container = document.body) { - let prompt, remove, - array = (options instanceof Array? options: [].slice.call(options)), - data = [...array]; - - switch(prompt_type) { - /* Allows the user to add and remove items from a list */ - case 'prompt': - case 'input': - remove = element => { - let prompter = document.queryBy('.web-to-plex-prompt').first, - header = document.queryBy('.web-to-plex-prompt-header').first, - counter = document.queryBy('.web-to-plex-prompt-options').first; - - if(element === true) - return prompter.remove(); - else - element.remove(); - - data.splice(+element.value, 1, null); - header.innerText = 'Approve ' + counter.children.length + (counter.children.length == 1?' item': ' items'); - }; - - prompt = document.furnish('div.web-to-plex-prompt', {}, - document.furnish('div.web-to-plex-prompt-body', {}, - // The prompt's title - document.furnish('h1.web-to-plex-prompt-header', {}, 'Approve ' + array.length + (array.length == 1? ' item': ' items')), - - // The prompt's items - document.furnish('div.web-to-plex-prompt-options', {}, - ...(ITEMS => { - let elements = []; - - for(let index = 0, length = ITEMS.length, ITEM; index < length; index++) - ITEM = ITEMS[index], - elements.push( - document.furnish('li.web-to-plex-prompt-option.mutable', { value: index, innerHTML: `${ ITEM.title }${ ITEM.year? ` (${ ITEM.year })`: '' } \u2014 ${ ITEM.type }` }, - document.furnish('button', { title: `Remove "${ ITEM.title }"`, onclick: event => { remove(event.target.parentElement); event.target.remove() } }) - ), - ); - - return elements - })(array) - ), - - // The engagers - document.furnish('div.web-to-plex-prompt-footer', {}, - document.furnish('input.web-to-plex-prompt-input[type=text]', { placeholder: 'Add an item (enter to add): Title (Year) Type / ID Type', title: 'Solo: A Star Wars Story (2018) movie / tt3778644 m', onkeydown: async event => { - if (event.keyCode === 13) { - let title, year, type, self = event.target, R = RegExp, - movie = /^(m(?:ovies?)?|f(?:ilms?)?|c(?:inemas?)?)/i, - Db, IMDbID, TMDbID, TVDbID, value = self.value; - - self.setAttribute('disabled', self.disabled = true); - self.value = `Searching for "${ value }"...`; - data = data.filter(value => value !== null && value !== undefined); - - if(/^\s*((?:tt)?\d+)(?:\s+(\w+)|\s*)?$/i.test(value)) { - let APIID = R.$1, - type = R.$2 || (data.length? data[0].type: 'movie'), - APIType = movie.test(type)? /^tt/i.test(APIID)? 'imdb': 'tmdb': 'tvdb'; - - type = movie.test(type)? 'movie': 'show'; - - Db = await getIDs({ type, APIID, APIType }); - IMDbID = Db.imdb; - TMDbID = Db.tmdb; - TVDbID = Db.tvdb; - - title = Db.title; - year = Db.year; - } else if(/^([^]+)(\s*\(\d{2,4}\)\s*|\s+\d{2,4}\s+)([\w\s\-]+)$/.test(value)) { - title = R.$1; - year = R.$2 || YEAR + ''; - type = R.$3 || (data.length? data[0].type: 'movie'); - - year = +year.replace(/\D/g, '').replace(/^\d{2}$/, '20$&'); - type = movie.test(type)? 'movie': 'show'; - - Db = await getIDs({ type, title, year }); - IMDbID = Db.imdb; - TMDbID = Db.tmdb; - TVDbID = Db.tvdb; - } - - event.preventDefault(); - if(type && title && !(/^(?:tt)?$/i.test(IMDbID || '') && /^0?$/.test(+TMDbID | 0) && /^0?$/.test(+TVDbID | 0))) { - remove(true); - new Prompt(prompt_type, [{ ...Db, type, IMDbID, TMDbID, TVDbID }, ...data], callback, container); - } else { - self.disabled = self.removeAttribute('disabled'); - self.value = value; - new Notification('error', `Couldn't find "${ value }"`); - } - } - } }), - document.furnish('button.web-to-plex-prompt-decline', { onclick: event => { remove(true); callback([]) } }, 'Close'), - document.furnish('button.web-to-plex-prompt-accept', { onclick: event => { remove(true); new Prompt(prompt_type, options, callback, container) } }, 'Reset'), - document.furnish('button.web-to-plex-prompt-accept', { onclick: event => { remove(true); callback(data.filter(value => value !== null && value !== undefined)) } }, 'Continue') - ) - ) - ); - break; - - /* Allows the user to remove predetermined items */ - case 'select': - remove = element => { - let prompter = document.queryBy('.web-to-plex-prompt').first, - header = document.queryBy('.web-to-plex-prompt-header').first, - counter = document.queryBy('.web-to-plex-prompt-options').first; - - if(element === true) - return prompter.remove(); - else - element.remove(); - - data.splice(+element.value, 1, null); - header.innerText = 'Approve ' + counter.children.length + (counter.children.length == 1?' item': ' items'); - }; - - prompt = document.furnish('div.web-to-plex-prompt', {}, - document.furnish('div.web-to-plex-prompt-body', {}, - // The prompt's title - document.furnish('h1.web-to-plex-prompt-header', {}, 'Approve ' + array.length + (array.length == 1? ' item': ' items')), - - // The prompt's items - document.furnish('div.web-to-plex-prompt-options', {}, - ...(ITEMS => { - let elements = []; - - for(let index = 0, length = ITEMS.length, ITEM; index < length; index++) - ITEM = ITEMS[index], - elements.push( - document.furnish('li.web-to-plex-prompt-option.mutable', { value: index, innerHTML: `${ ITEM.title }${ ITEM.year? ` (${ ITEM.year })`: '' } \u2014 ${ ITEM.type }` }, - document.furnish('button', { title: `Remove "${ ITEM.title }"`, onclick: event => { remove(event.target.parentElement); event.target.remove() } }) - ), - ); - - return elements - })(array) - ), - - // The engagers - document.furnish('div.web-to-plex-prompt-footer', {}, - document.furnish('button.web-to-plex-prompt-decline', { onclick: event => { remove(true); callback([]) } }, 'Close'), - document.furnish('button.web-to-plex-prompt-accept', { onclick: event => { remove(true); new Prompt(prompt_type, options, callback, container) } }, 'Reset'), - document.furnish('button.web-to-plex-prompt-accept', { onclick: event => { remove(true); callback(data.filter(value => value !== null && value !== undefined)) } }, 'Continue') - ) - ) - ); - break; - - default: - return terminal.warn(`Unknown prompt type "${ prompt_type }"`); - break; - } - - return container.append(prompt), prompt; - } -} + // open up the options page + function Options() { + chrome.runtime.sendMessage({ + type: 'OPEN_OPTIONS' + }); + } -// Send an update query to background.js -function sendUpdate(type, options = {}) { - terminal.log(`Requesting update: ${ type }`, options); + // "secret frame" + function sFrame(url, callbacks) { + let { success, error } = callbacks; - chrome.runtime.sendMessage({ - type, - options - }); -} + let frame = document.furnish('iframe#web-to-plex-sframe', { + src: url, + style: ` + display: none !important; + opacity: 0 !important; + visibility: hidden !important; + `, -// get the saved options -function $getOptions() { - return new Promise((resolve, reject) => { - function handleOptions(options) { - if ((!options.plexToken || !options.servers) && !options.DO_NOT_USE) - return reject(new Error('Required options are missing')), - null; - - let server, o; - - if (!options.DO_NOT_USE) { - // For now we support only one Plex server, but the options already - // allow multiple for easy migration in the future. - server = options.servers[0]; - o = { - server: { - ...server, - // Compatibility for users who have not updated their settings yet. - connections: server.connections || [{ uri: server.url }] - }, - ...options - }; - - options.plexURL = o.plexURL? - `${ o.plexURL }web#!/server/${ o.server.id }/`: - `https://app.plex.tv/web/app#!/server/${ o.server.id }/`; - } else { - o = options; - } - - if (o.couchpotatoBasicAuthUsername) - o.couchpotatoBasicAuth = { - username: o.couchpotatoBasicAuthUsername, - password: o.couchpotatoBasicAuthPassword - }; - - // TODO: stupid copy/pasta - if (o.watcherBasicAuthUsername) - o.watcherBasicAuth = { - username: o.watcherBasicAuthUsername, - password: o.watcherBasicAuthPassword - }; - - if (o.radarrBasicAuthUsername) - o.radarrBasicAuth = { - username: o.radarrBasicAuthUsername, - password: o.radarrBasicAuthPassword - }; - - if (o.sonarrBasicAuthUsername) - o.sonarrBasicAuth = { - username: o.sonarrBasicAuthUsername, - password: o.sonarrBasicAuthPassword - }; - - if (o.ombiURLRoot && o.ombiToken) { - o.ombiURL = o.ombiURLRoot; - } else { - delete o.ombiURL; // prevent variable ghosting - } - - if (o.couchpotatoURLRoot && o.couchpotatoToken) { - o.couchpotatoURL = `${ items.couchpotatoURLRoot }/api/${encodeURIComponent(o.couchpotatoToken)}`; - } else { - delete o.couchpotatoURL; // prevent variable ghosting - } - - if (o.watcherURLRoot && o.watcherToken) { - o.watcherURL = o.watcherURLRoot; - } else { - delete o.watcherURL; // prevent variable ghosting - } - - if (o.radarrURLRoot && o.radarrToken) { - o.radarrURL = o.radarrURLRoot; - } else { - delete o.radarrURL; // prevent variable ghosting - } - - if (o.sonarrURLRoot && o.sonarrToken) { - o.sonarrURL = o.sonarrURLRoot; - } else { - delete o.sonarrURL; // prevent variable ghosting - } - - resolve(o); - } - - storage.get(null, options => { - if (chrome.runtime.lastError) - chrome.storage.local.get(null, handleOptions); - else - handleOptions(options); - }); - }); -} + onload: success, + onerror: error, + }); -// self explanatory -function openOptionsPage() { - chrome.runtime.sendMessage({ - type: 'OPEN_OPTIONS' - }); -} + // todo: make iframe, load, delete + document.body.append(frame); + } -// self explanatory, returns an object -function parseOptions() { - return $getOptions() - .then( - options => (config = options), - error => { - new Notification( - 'warning', - 'Fill in missing Web to Plex options', - 15000, - openOptionsPage - ); - throw error; - } - ); -} + // Send an update query to background.js + Update = (type, options = {}, postToo) => { + if(configuration) + console.log(`Requesting update [post-to-top: ${ !!postToo }]: ${ type }`, options); + else if(!Update.retry) + try { + configuration = ParsedOptions(); -let config = parseOptions(), - AUTO_GRAB = { - ENABLED: config.UseAutoGrab, - LIMIT: config.AutoGrabLimit, - }; + Update.retry = true; -function HandleProxyHeaders(Headers = "", URL = "") { - let headers = {}; + Update(type, options, postToo); + } catch(error) { + console.warn(`Update failed... "${ error }" Attempting to save configuration...`); - Headers.replace(/^[ \t]*([^\=\s]+)[ \t]*=[ \t]*((["'`])(?:[^\\\3]*|\\.)\3|[^\f\n\r\v]*)/gm, ($0, $1, $2, $3, $$, $_) => { - let string = !!$3; + return sFrame(extURL(`options/index.html#save`), { + success: async event => { + let self = event.target; - if(string) { - headers[$1] = $2.replace(RegExp(`^${ $3 }|${ $3 }$`, 'g'), ''); - } else { - $2 = $2.replace(/@([\w\.]+)/g, (_0, _1, _$, __) => { - let path = _1.split('.'), property = top; + await ParsedOptions(); - for(let index = 0, length = path.length; index < length; index++) - property = property[path[index]]; + Update(type, options, postToo); - headers[$1] = property; - }) - .replace(/@\{b(ase-?)?64-url\}/gi, btoa(URL)) - .replace(/@\{enc(ode)?-url\}/gi, encodeURIComponent(URL)) - .replace(/@\{(raw-)?url\}/gi, URL); - } - }); + self.remove(); + }, - return headers; -} + error: async event => { + let self = event.target; + self.remove(); -// fetch/search for the item's media ID(s) -async function getIDs({ title, year, type, IMDbID, TMDbID, TVDbID, APIType, APIID, meta, rerun }) { - let json = {}, // returned object - data = {}, // mutated object - promise, // query promise - api = { - tmdb: config.TMDbAPI || 'bcb95f026f9a01ffa707fcff71900e94', - omdb: config.OMDbAPI || 'PlzBanMe', - ombi: config.ombiToken, - }, - apit = APIType || type, // api type (depends on "rqut") - apid = APIID || null, // api id - iid = IMDbID || null, // IMDbID - mid = TMDbID || null, // TMDbID - tid = TVDbID || null, // TVDbID - rqut = apit, // request type: tmdb, imdb, or tvdb - manable = config.ManagerSearch && !rerun; // is the user's "Manager Searches" option enabled? - - type = type || null; - meta = { ...meta, mode: 'cors' }; - rqut = - /(tv|show|series)/i.test(rqut)? - 'tvdb': - /(movie|film)/i.test(rqut)? - 'tmdb': - rqut || '*'; - manable = manable && (config.ombiURL || (config.radarrURL && rqut == 'tmdb') || (config.sonarrURL && rqut == 'tvdb')); - title = (title? title.replace(/\s*[\:,]\s*Season\s+\d+.*$/i, '').toCaps(): "") - .replace(/\u201a/g, ',') // fancy comma - .replace(/[\u2019\u201b]/g, "'") // fancy apostrophe - .replace(/[\u201c\u201d]/g, '"') // fancy quotation marks - .replace(/[^\u0000-\u00ff]+/g, ''); // only accept UTF-8 characters - year = year? (year + '').replace(/\D+/g, ''): year; - - let plus = (string, character = '+') => string.replace(/\s+/g, character); - - let local, savename; - - if(year) { - savename = `${title} (${year}).${rqut}`.toLowerCase(), - local = await load(savename); - } else { - year = await load(`${title}.${rqut}`.toLowerCase()) || year; - `${title} (${year}).${rqut}`.toLowerCase(); - local = await load(savename); - } - - if(local) { - terminal.log('[LOCAL] Search results', local); - return local; - } - - /* the rest of this function is a beautiful mess that will need to be dealt with later... but it works */ - let url = - (manable && title && config.ombiURLRoot)? - `${ config.ombiURLRoot }api/v1/Search/${ (rqut == 'imdb' || rqut == 'tmdb' || apit == 'movie')? 'movie': 'tv' }/${ plus(title, '%20') }/?apikey=${ api.ombi }`: - (manable && (config.radarrURLRoot || config.sonarrURLRoot))? - (config.radarrURLRoot && (rqut == 'imdb' || rqut == 'tmdb'))? - (mid)? - `${ config.radarrURLRoot }api/movie/lookup/tmdb?tmdbId=${ mid }&apikey=${ config.radarrToken }`: - (iid)? - `${ config.radarrURLRoot }api/movie/lookup/imdb?imdbId=${ iid }&apikey=${ config.radarrToken }`: - `${ config.radarrURLRoot }api/movie/lookup?term=${ plus(title, '%20') }&apikey=${ config.radarrToken }`: - (tid)? - `${ config.sonarrURLRoot }api/series/lookup?term=tvdb:${ tid }&apikey=${ config.sonarrToken }`: - `${ config.sonarrURLRoot }api/series/lookup?term=${ plus(title, '%20') }&apikey=${ config.sonarrToken }`: - (rqut == 'imdb' || (rqut == '*' && !iid && title) || (rqut == 'tvdb' && !iid && title && rerun))? - (iid)? - `https://www.omdbapi.com/?i=${ iid }&apikey=${ api.omdb }`: - (year)? - `https://www.omdbapi.com/?t=${ plus(title) }&y=${ year }&apikey=${ api.omdb }`: - `https://www.omdbapi.com/?t=${ plus(title) }&apikey=${ api.omdb }`: - (rqut == 'tmdb' || (rqut == '*' && !mid && title && year) || apit == 'movie')? - (apit && apid)? - `https://api.themoviedb.org/3/${ apit }/${ apid }?api_key=${ api.tmdb }`: - (iid)? - `https://api.themoviedb.org/3/find/${ iid || mid || tid }?api_key=${ api.tmdb }&external_source=${ iid? 'imdb': mid? 'tmdb': 'tvdb' }_id`: - `https://api.themoviedb.org/3/search/${ apit }?api_key=${ api.tmdb }&query=${ encodeURI(title) }${ year? '&year=' + year: '' }`: - (rqut == 'tvdb' || (rqut == '*' && !tid && title) || (apid == tid))? - (tid)? - `https://api.tvmaze.com/shows/?thetvdb=${ tid }`: - (iid)? - `https://api.tvmaze.com/shows/?imdb=${ iid }`: - `https://api.tvmaze.com/search/shows?q=${ encodeURI(title) }`: - (title)? - (apit && year)? - `https://www.theimdbapi.org/api/find/${ apit }?title=${ encodeURI(title) }&year=${ year }`: - `https://www.theimdbapi.org/api/find/movie?title=${ encodeURI(title) }${ year? '&year=' + year: '' }`: - null; - - if(url === null) return null; - - let proxy = config.proxy, - cors = proxy.url, // if cors is requried and not uspported, proxy through this URL - headers = HandleProxyHeaders(proxy.headers, url); - - if(proxy.enabled && /(^http:\/\/)(?!localhost|127\.0\.0\.1(?:\/8)?|::1(?:\/128)?|:\d+)\b/i.test(url)) { - url = cors - .replace(/\{b(ase-?)?64-url\}/gi, btoa(url)) - .replace(/\{enc(ode)?-url\}/gi, encodeURIComponent(url)) - .replace(/\{(raw-)?url\}/gi, url); - - terminal.log({ proxy, url, headers }); - } - - terminal.log(`Searching for "${ title } (${ year })" in ${ type || apit }/${ rqut }${ proxy.enabled? '[PROXY]': '' } => ${ url }`); - - await(proxy? fetch(url, { mode: "cors", headers }): fetch(url)) - .then(response => response.text()) - .then(data => { - try { - if(data) - json = JSON.parse(data); - } catch(error) { - terminal.error(`Failed to parse JSON: "${ data }"`); - } - }) - .catch(error => { - throw error; - }); - - terminal.log('Search results', { title, year, url, json }); - - if('results' in json) - json = json.results; - - if(json instanceof Array) { - let b = { release_date: '', year: '' }, - t = (s = "") => s.toLowerCase(), - c = (s = "") => t(s).replace(/\&/g, 'and').replace(/\W+/g, ''), - k = (s = "") => { - - let r = [ - [/(?!^\s*)\b(show|series|a([st]|nd?|cross|fter|lthough)?|b(e(cause|fore|tween)|ut|y)|during|from|in(to)?|[io][fn]|[fn]?or|the|[st]o|through|under|with(out)?|yet)\b\s*/gi, ''], - // try replacing common words, e.g. Conjunctions, "Show," "Series," etc. - [/\s+/g, '|'] - ]; - - for(let i = 0; i < r.length; i++) { - if(/^([\(\|\)]+)?$/.test(s)) return ""; - - s = s.replace(r[i][0], r[i][1]); - } - - return c(s); - }, - R = (s = "", S = "", n = !0) => { - let score = 100 * ((S.match(RegExp(`\\b(${k(s)})\\b`, 'i')) || [null]).length / (S.split(' ').length || 1)), - passing = config.UseLooseScore | 0; - - return (S != '' && score >= passing) || (n? R(S, s, !n): n); - }, - en = /^(u[ks]-?|utf8-?)?en(glish)?$/i; - - // Find an exact match: Title (Year) | #IMDbID - let index, found, $data, lastscore; - for(index = 0, found = false, $data, lastscore = 0; (title && year) && index < json.length && !found; index++) { - $data = json[index]; - - let altt = $data.alternativeTitles, - $alt = (altt && altt.length? altt.filter(v => t(v) == t(title))[0]: null); - - // Radarr & Sonarr - if(manable) - found = ((t($data.title) == t(title) || $alt) && +year === +$data.year)? - $alt || $data: - found; - //api.tvmaze.com/ - else if(('externals' in ($data = $data.show || $data) || 'show' in $data) && $data.premiered) - found = (iid == $data.externals.imdb || t($data.name) == t(title) && year == $data.premiered.slice(0, 4))? - $data: - found; - //api.themoviedb.org/ \local - else if(('movie_results' in $data || 'tv_results' in $data || 'results' in $data) && $data.release_date) - found = (DATA => { - if(DATA.results) - if(rqut == 'tmdb') - DATA.movie_results = DATA.results; - else - DATA.tv_results = DATA.results; - - let i, f, o, l; - - for(i = 0, f = !1, o = DATA.movie_results, l = o.length | 0; i < l; i++) - f = (t(o.title) === t(title) && o.release_date.slice(0, 4) == year); - - for(i = (+f * l), o = (f? o: DATA.tv_results), l = (f? l: o.length | 0); i < l; i++) - f = (t(o.name) === t(title) && o.first_air_date.slice(0, 4) == year); - - return f? o: f = !!iid; - })($data); - //api.themoviedb.org/ \remote - else if(('original_name' in $data || 'original_title' in $data) && $data.release_date) - found = (tid == $data.id || (t($data.original_name || $data.original_title) == t(title) || t($data.name) == t(title)) && year == ($data || b).release_date.slice(0, 4))? - $data: - found; - //theimdbapi.org/ - else if($data.release_date) - found = (t($data.title) === t(title) && year == ($data.url || $data || b).release_date.slice(0, 4))? - $data: - found; - -// terminal.log(`Strict Matching: ${ !!found }`, !!found? found: null); - } - - // Find a close match: Title - for(index = 0; title && index < json.length && (!found || lastscore > 0); index++) { - $data = json[index]; - - let altt = $data.alternativeTitles, - $alt = (altt && altt.length? altt.filter(v => c(v) == c(title)): null); - - // Radarr & Sonarr - if(manable) - found = (c($data.title) == c(title) || $alt)? - $alt || $data: - found; - //api.tvmaze.com/ - else if('externals' in ($data = $data.show || $data) || 'show' in $data) - found = - // ignore language barriers - (c($data.name) == c(title))? - $data: - // trust the api matching - ($data.score > lastscore)? - (lastscore = $data.score || $data.vote_count, $data): - found; - //api.themoviedb.org/ \local - else if('movie_results' in $data || 'tv_results' in $data || 'results' in $data) - found = (DATA => { - let i, f, o, l; - - if(DATA.results) - if(rqut == 'tmdb') - DATA.movie_results = DATA.results; - else - DATA.tv_results = DATA.results; - - for(i = 0, f = !1, o = DATA.movie_results, l = o.length | 0; i < l; i++) - f = (c(o.title) == c(title)); - - for(i = (+f * l), o = (f? o: DATA.tv_results), l = (f? l: o.length | 0); i < l; i++) - f = (c(o.name) == c(title)); - - return f? o: f; - })($data); - //api.themoviedb.org/ \remote - else if('original_name' in $data || 'original_title' in $data || 'name' in $data) - found = (c($data.original_name || $data.original_title || $data.name) == c(title))? - $data: - found; - //theimdbapi.org/ - else if(en.test($data.language)) - found = (c($data.title) == c(title))? - $data: - found; - -// terminal.log(`Title Matching: ${ !!found }`, !!found? found: null); - } - - // Find an OK match (Loose Searching): Title ~ Title - // The examples below are correct - // GOOD, found: VRV's "Bakemonogatari" vs. TVDb's "Monogatari Series" - // /\b(monogatari)\b/i.test('bakemonogatari') === true - // this is what this option is for - // OK, found: "The Title of This is Bad" vs. "The Title of This is OK" (this is semi-errornous) - // /\b(title|this|bad)\b/i.test('title this ok') === true - // this may be a possible match, but it may also be an error: 'title' and 'this' - // BAD, not found: "Gun Show Showdown" vs. "Gundarr" - // /\b(gun|showdown)\b/i.test('gundarr') === false - // this should not match; the '\b' (border between \w and \W) keeps them from matching - for(index = 0; config.UseLoose && title && index < json.length && (!found || lastscore > 0); index++) { - $data = json[index]; - - let altt = $data.alternativeTitles, - $alt = (altt && altt.length? altt.filter(v => R(v, title)): null); - - // Radarr & Sonarr - if(manable) - found = (R($data.name, title) || $alt)? - $alt || $data: - found; - //api.tvmaze.com/ - else if('externals' in ($data = $data.show || $data) || 'show' in $data) - found = - // ignore language barriers - (R($data.name, title) || terminal.log('Matching:', [$data.name, title], R($data.name, title)))? - $data: - // trust the api matching - ($data.score > lastscore)? - (lastscore = $data.score, $data): - found; - //api.themoviedb.org/ \local - else if('movie_results' in $data || 'tv_results' in $data) - found = (DATA => { - let i, f, o, l; - - for(i = 0, f = !1, o = DATA.movie_results, l = o.length | 0; i < l; i++) - f = R(o.title, title); - - for(i = (+f * l), o = (f? o: DATA.tv_results), l = (f? l: o.length | 0); i < l; i++) - f = R(o.name, title); - - return f? o: f; - })($data); - //api.themoviedb.org/ \remote - else if('original_name' in $data || 'original_title' in $data) - found = (R($data.original_name, title) || R($data.original_title, title) || R($data.name, title))? - $data: - found; - //theimdbapi.org/ - else if(en.test($data.language)) - found = (R($data.title, title))? - $data: - found; - -// terminal.log(`Loose Matching: ${ !!found }`, !!found? found: null); - } - - json = found; - } - - if((json === undefined || json === null || json === false) && !rerun) - return json = getIDs({ title, year: YEAR, type, IMDbID, TMDbID, TVDbID, APIType, APIID, meta, rerun: true }); - else if((json === undefined || json === null)) - json = { IMDbID, TMDbID, TVDbID }; - - let ei = 'tt', - mr = 'movie_results', - tr = 'tv_results'; - - json = json && mr in json? json[mr].length > json[tr].length? json[mr]: json[tr]: json; - - if(json instanceof Array) - json = json[0]; - - if(!json) - json = { IMDbID, TMDbID, TVDbID }; - - // Ombi, Radarr and Sonarr - if(manable) - data = { - imdb: iid || json.imdbId || ei, - tmdb: mid || json.tmdbId || json.theMovieDbId | 0, - tvdb: tid || json.tvdbId || json.theTvDbId | 0, - title: json.title || title, - year: +(json.year || year) - }; - //api.tvmaze.com/ - else if('externals' in (json = json.show || json)) - data = { - imdb: iid || json.externals.imdb || ei, - tmdb: mid || json.externals.themoviedb | 0, - tvdb: tid || json.externals.thetvdb | 0, - title: json.name || title, - year: ((json.premiered || json.first_aired_date || year) + '').slice(0, 4) - }; - //api.themoviedb.org/ - else if('imdb_id' in (json = mr in json? json[mr].length > json[tr].length? json[mr]: json[tr]: json) || 'original_name' in json || 'original_title' in json) - data = { - imdb: iid || json.imdb_id || ei, - tmdb: mid || json.id | 0, - tvdb: tid || json.tvdb | 0, - title: json.title || json.name || title, - year: ((json.release_date || json.first_air_date || year) + '').slice(0, 4) - }; - //omdbapi.com/ - else if('imdbID' in json) - data = { - imdb: iid || json.imdbID || ei, - tmdb: mid || json.tmdbID | 0, - tvdb: tid || json.tvdbID | 0, - title: json.Title || json.Name || title, - year: json.Year || year - }; - //theapache64.com/movie_db/ - else if('data' in json) - data = { - imdb: iid || json.data.imdb_id || ei, - tmdb: mid || json.data.tmdb_id | 0, - tvdb: tid || json.data.tvdb_id | 0, - title: json.data.name || json.data.title || title, - year: json.data.year || year - }; - //theimdbapi.org/ - else if('imdb' in json) - data = { - imdb: iid || json.imdb || ei, - tmdb: mid || json.id | 0, - tvdb: tid || json.tvdb | 0, - title, - year - }; - // given by the requesting service - else - data = { - imdb: iid || ei, - tmdb: mid | 0, - tvdb: tid | 0, - title, - year - }; - - year = +((data.year + '').slice(0, 4)) || 0; - data.year = year; - - let best = { title, year, data, type, rqut, score: json.score | 0 }; - - terminal.log('Best match:', url, { best, json }); - - if(best.data.imdb == ei && best.data.tmdb == 0 && best.data.tvdb == 0) - return terminal.log(`No information was found for "${ title } (${ year })"`), {}; - - save(savename, data); // e.g. "Coco (0)" on Netflix before correction / no repeat searches - save(savename = `${title} (${year}).${rqut}`.toLowerCase(), data); // e.g. "Coco (2017)" on Netflix after correction / no repeat searches - save(`${title}.${rqut}`.toLowerCase(), year); - - terminal.log(`Saved as "${ savename }"`, data); - - return data; -} + new Notification('error', `Fill in missing Web to Plex options`, 15000, Options, false); -function $pushAddToCouchpotato(options) { - // TODO: this does not work anymore! - if (!options.IMDbID) - return new Notification( - 'warning', - 'Stopped adding to CouchPotato: No IMDb ID' - ); + throw `Unable to set configuration variable: ${ JSON.stringify(configuration) }`; + } + }); + } + else + return Update.retry = false; + + chrome.runtime.sendMessage({ + type, + options + }); + + if(postToo) + top.postMessage(options); + }; + + // get the saved options + function options() { + return new Promise((resolve, reject) => { + function handleOptions(options) { + if((!options.plexToken || !options.servers) && !options.IGNORE_PLEX) + return reject(new Error('Required options are missing')), + null; + + let server, o; + + if(!options.IGNORE_PLEX) { + // For now we support only one Plex server, but the options already + // allow multiple for easy migration in the future. + server = options.servers[0]; + o = { + server: { + ...server, + // Compatibility for users who have not updated their settings yet. + connections: server.connections || [{ uri: server.url }] + }, + ...options + }; + + options.plexURL = o.plexURL? + `${ o.plexURL }web#!/server/${ o.server.id }/`: + `https://app.plex.tv/web/app#!/server/${ o.server.id }/`; + } else { + o = options; + } + + if(o.couchpotatoBasicAuthUsername) + o.couchpotatoBasicAuth = { + username: o.couchpotatoBasicAuthUsername, + password: o.couchpotatoBasicAuthPassword + }; + + // TODO: stupid copy/pasta + if(o.watcherBasicAuthUsername) + o.watcherBasicAuth = { + username: o.watcherBasicAuthUsername, + password: o.watcherBasicAuthPassword + }; + + if(o.radarrBasicAuthUsername) + o.radarrBasicAuth = { + username: o.radarrBasicAuthUsername, + password: o.radarrBasicAuthPassword + }; + + if(o.sonarrBasicAuthUsername) + o.sonarrBasicAuth = { + username: o.sonarrBasicAuthUsername, + password: o.sonarrBasicAuthPassword + }; + + if(o.medusaBasicAuthUsername) + o.medusaBasicAuth = { + username: o.medusaBasicAuthUsername, + password: o.medusaBasicAuthPassword + }; + + if(o.sickBeardBasicAuthUsername) + o.sickBeardBasicAuth = { + username: o.sickBeardBasicAuthUsername, + password: o.sickBeardBasicAuthPassword + }; + + if(o.usingOmbi && o.ombiURLRoot && o.ombiToken) { + o.ombiURL = o.ombiURLRoot; + } else { + delete o.ombiURL; // prevent variable ghosting + } + + if(o.usingCouchPotato && o.couchpotatoURLRoot && o.couchpotatoToken) { + o.couchpotatoURL = `${ items.couchpotatoURLRoot }/api/${encodeURIComponent(o.couchpotatoToken)}`; + } else { + delete o.couchpotatoURL; // prevent variable ghosting + } + + if(o.usingWatcher && o.watcherURLRoot && o.watcherToken) { + o.watcherURL = o.watcherURLRoot; + } else { + delete o.watcherURL; // prevent variable ghosting + } + + if(o.usingRadarr && o.radarrURLRoot && o.radarrToken) { + o.radarrURL = o.radarrURLRoot; + } else { + delete o.radarrURL; // prevent variable ghosting + } + + if(o.usingSonarr && o.sonarrURLRoot && o.sonarrToken) { + o.sonarrURL = o.sonarrURLRoot; + } else { + delete o.sonarrURL; // prevent variable ghosting + } + + if(o.usingMedusa && o.medusaURLRoot && o.medusaToken) { + o.medusaURL = o.medusaURLRoot; + } else { + delete o.medusaURL; // prevent variable ghosting + } + + if(o.usingSickBeard && o.sickBeardURLRoot && o.sickBeardToken) { + o.sickBeardURL = o.sickBeardURLRoot; + } else { + delete o.sickBeardURL; // prevent variable ghosting + } + + resolve(o); + } - chrome.runtime.sendMessage( - { - type: 'VIEW_COUCHPOTATO', - url: `${ config.couchpotatoURL }/media.get`, - IMDbID: options.IMDbID, - TMDbID: options.TMDbID, - TVDbID: options.TVDbID, - basicAuth: config.couchpotatoBasicAuth, + UTILS_STORAGE.get(null, options => { + if(chrome.runtime.lastError) + chrome.storage.local.get(null, handleOptions); + else + handleOptions(options); + }); + }); + } + + // self explanatory, returns an object; sets the configuration variable + function ParsedOptions() { + return options() + .then( + options => { + configuration = {}; + + /* Don't expose the user's authentication information to sites */ + for(let key in options) + if(/username|password|token|api|server|url|storage|cache/i.test(key)) + if(ALLOWED && RegExp(PERMISS.join('|'),'i').test(key)) + configuration[key] = options[key]; + else + /* Do nothing */; + // else if(/(^cache-data|paths|qualities)/i.test(key)) + // /* Pre-parse JSON - make sure anything accessing thedata handles objects too */ + // configuration[key] = JSON.parse(options[key] || null); + else + /* Simple copy */ + configuration[key] = options[key]; + + CAUGHT = JSON.parse(options.__caught); + CAUGHT.bump = async(ids) => { + for(let id in ids) + CAUGHT[id.toLowerCase().slice(0, 4)].push(ids[id]); + + let __caught = JSON.stringify(CAUGHT); + + await UTILS_STORAGE.set({ __caught }, () => configuration.__caught = __caught); + }; + + return __CONFIG__ = options; + }, + error => { + new Notification( + 'warning', + 'Fill in missing Web to Plex options', + 15000, + Options + ); + throw error; + } + ); + } + + await ParsedOptions(); + + let AUTO_GRAB = { + ENABLED: __CONFIG__.UseAutoGrab, + LIMIT: __CONFIG__.AutoGrabLimit, }, - response => { - let movieExists = response.success; - if (response.error) { - return new Notification( + UTILS_DEVELOPER = __CONFIG__.DeveloperMode, // = { true: Developer Mode, fase: Standard Mode } + UTILS_TERMINAL = + UTILS_DEVELOPER? + console: + { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m, honor: m => m }; + + UTILS_TERMINAL.honor = UTILS_TERMINAL.honor? + UTILS_TERMINAL.honor: + (...messages) => { + if(messages.length == 1) { + let message = messages[0], + type = typeof message == 'object'? 'o': 'c'; + + UTILS_TERMINAL.log( + (type == 'o'? message: `%${ type }>> ${ message } <<`), + ( + type == 'o'? + null: + ` + background-color: #00332b; + border-bottom: 1px solid #0000; + border-top: 1px solid #065; + box-sizing: border-box; + clear: right; + color: #f5f5f5; + display: block !important; + line-height: 2; + user-select: text; + + flex-basis: 1; + flex-shrink: 1; + + margin: 0; + overflow-wrap: break-word; + pading: 3px 22px 1px 0; + position: fixed; + z-index: -1; + + min-height: 0; + min-width: 100%; + height: 100%; + width: 100%; + ` + ) + ); + } else { + messages.forEach(message => UTILS_TERMINAL.honor(message)); + } + }; + + if(configuration) { + let host = location.host.replace(/^(ww\w+\.)/, ''), + doms = configuration.__domains.split(','); + + if(!~doms.indexOf(host)) + return; + } + + UTILS_TERMINAL.log('UTILS_DEVELOPER:', UTILS_DEVELOPER, configuration); + + // parse the formatted headers and URL + function HandleProxyHeaders(Headers = "", URL = "") { + let headers = {}; + + Headers.replace(/^[ \t]*([^\=\s]+)[ \t]*=[ \t]*((["'`])(?:[^\\\3]*|\\.)\3|[^\f\n\r\v]*)/gm, ($0, $1, $2, $3, $$, $_) => { + let string = !!$3; + + if(string) { + headers[$1] = $2.replace(RegExp(`^${ $3 }|${ $3 }$`, 'g'), ''); + } else { + $2 = $2.replace(/@([\w\.]+)/g, (_0, _1, _$, __) => { + let path = _1.split('.'), property = top; + + for(let index = 0, length = path.length; index < length; index++) + property = property[path[index]]; + + headers[$1] = property; + }) + .replace(/@\{b(ase-?)?64-url\}/gi, btoa(URL)) + .replace(/@\{enc(ode)?-url\}/gi, encodeURIComponent(URL)) + .replace(/@\{(raw-)?url\}/gi, URL); + } + }); + + return headers; + } + + // fetch/search for the item's media ID(s) + // rerun enum - [0bWXYZ] - [Tried Different URL | Tried Matching Title | Tried Loose Searching | Tried Rerunning Altogether] + async function Identify({ title, alttitle, year, type, IMDbID, TMDbID, TVDbID, APIType, APIID, meta, rerun }) { + let json = {}, // returned object + data = {}, // mutated object + promise, // query promise + api = { + tmdb: __CONFIG__.TMDbAPI || '37930f472ee15263f0b1ef5cc72e181a', + omdb: __CONFIG__.OMDbAPI || 'PlzBanMe', + ombi: __CONFIG__.ombiToken, + }, + apit = APIType || type, // api type (depends on "rqut") + apid = APIID || null, // api id + iid = IMDbID || null, // IMDbID + mid = TMDbID || null, // TMDbID + tid = TVDbID || null, // TVDbID + rqut = apit, // request type: tmdb, imdb, or tvdb + manable = __CONFIG__.ManagerSearch && !(rerun & 0b1000), // is the user's "Manager Searches" option enabled? + UTF_16 = /[^0\u0020-\u007e, 1\u00a1\u00bf-\u00ff, 2\u0100-\u017f, 3\u0180-\u024f, 4\u0300-\u036f, 5\u0370-\u03ff, 6\u0400-\u04ff, 7\u0500-\u052f, 8\u20a0-\u20bf]+/g; + + type = type || null; + meta = { ...meta, mode: 'cors' }; + rqut = + /(tv|show|series)/i.test(rqut)? + 'tvdb': + /(movie|film|cinema)s?/i.test(rqut)? + 'tmdb': + rqut || '*'; + manable = manable && (__CONFIG__.usingOmbi || (__CONFIG__.usingRadarr && rqut == 'tmdb') || ((__CONFIG__.usingSonarr || __CONFIG__.usingMedusa /*|| __CONFIG__.usingSickBeard*/) && rqut == 'tvdb')); + title = (title? title.replace(/\s*[\:,]\s*seasons?\s+\d+.*$/i, '').toCaps(): "") + .replace(/[\u2010-\u2015]/g, '-') // fancy hyphen + .replace(/[\u201a\u275f]/g, ',') // fancy comma + .replace(/[\u2018\u2019\u201b\u275b\u275c`]/g, "'") // fancy apostrophe (tilde from anime results by TMDb) + .replace(/[\u201c-\u201f\u275d\u275e]/g, '"') // fancy quotation marks + .replace(UTF_16, ''); // only accept "usable" characters + /* 0[ -~], 1[¡¿-ÿ], 2[Ā-ſ], 3[ƀ-ɏ], 4[ò-oͯ], 5[Ͱ-Ͽ], 6[Ѐ-ӿ], 7[Ԁ-ԯ], 8[₠-₿] */ + /** Symbol Classes + 0) Basic Latin, and standard characters + 1) Latin (Supplement) + 2) Latin Extended I + 3) Latin Extended II + 4) Diatrical Marks + 5) Greek & Coptic + 6) Basic Cyrillic + 7) Cyrillic (Supplement) + 8) Currency Symbols + */ + year = year? (year + '').replace(/\D+/g, ''): year; + + let plus = (string, character = '+') => string.replace(/\s+/g, character); + + let local, savename; + + if(year) { + savename = `${title} (${year}).${rqut}`.toLowerCase(), + local = await load(savename); + } else { + year = await load(`${title}.${rqut}`.toLowerCase()) || year; + savename = `${title} (${year}).${rqut}`.toLowerCase(); + local = await load(savename); + } + + if(local) { + UTILS_TERMINAL.honor('[LOCAL] Search results', local); + return local; + } + + /* the rest of this function is a beautiful mess that will need to be dealt with later... but it works */ + let url = + (manable && title && __CONFIG__.usingOmbi)? + `${ __CONFIG__.ombiURLRoot }api/v1/Search/${ (rqut == 'imdb' || rqut == 'tmdb' || apit == 'movie')? 'movie': 'tv' }/${ plus(title, '%20') }/?apikey=${ api.ombi }`: + (manable && (__CONFIG__.usingRadarr || __CONFIG__.usingSonarr || __CONFIG__.usingMedusa /*|| __CONFIG__.usingSickBeard*/))? + (__CONFIG__.usingRadarr && (rqut == 'imdb' || rqut == 'tmdb'))? + (mid)? + `${ __CONFIG__.radarrURLRoot }api/movie/lookup/tmdb?tmdbId=${ mid }&apikey=${ __CONFIG__.radarrToken }`: + (iid)? + `${ __CONFIG__.radarrURLRoot }api/movie/lookup/imdb?imdbId=${ iid }&apikey=${ __CONFIG__.radarrToken }`: + `${ __CONFIG__.radarrURLRoot }api/movie/lookup?term=${ plus(title, '%20') }&apikey=${ __CONFIG__.radarrToken }`: + (__CONFIG__.usingSonarr)? + (tid)? + `${ __CONFIG__.sonarrURLRoot }api/series/lookup?term=tvdb:${ tid }&apikey=${ __CONFIG__.sonarrToken }`: + `${ __CONFIG__.sonarrURLRoot }api/series/lookup?term=${ plus(title, '%20') }&apikey=${ __CONFIG__.sonarrToken }`: + (__CONFIG__.usingMedusa)? + (tid)? + `${ __CONFIG__.medusaURLRoot }api/v2/series/tvdb${ tid }?detailed=true&api_key=${ __CONFIG__.medusaToken }`: + `${ __CONFIG__.medusaURLRoot }api/v2/internal/searchIndexersForShowName?query=${ plus(title) }&indexerId=0&api_key=${ __CONFIG__.medusaToken }`: + /* TODO: find a way to get CORS to work on Sick Beard URLs (localhost) */ + // (__CONFIG__.usingSickBeard)? + // (tid)? + // `${ __CONFIG__.sickBeardURLRoot }api/${ __CONFIG__.sickBeardToken }/?cmd=sb.searchtvdb&tvdbid=${ tid }`: + // `${ __CONFIG__.sickBeardURLRoot }api/${ __CONFIG__.sickBeardToken }/?cmd=sb.searchtvdb&name=${ encodeURIComponent(title) }`: + null: + (rqut == 'imdb' || (rqut == '*' && !iid && title) || (rqut == 'tvdb' && !iid && title && !(rerun & 0b1000)) && (rerun |= 0b1000))? + (iid)? + `https://www.omdbapi.com/?i=${ iid }&apikey=${ api.omdb }`: + (year)? + `https://www.omdbapi.com/?t=${ plus(title) }&y=${ year }&apikey=${ api.omdb }`: + `https://www.omdbapi.com/?t=${ plus(title) }&apikey=${ api.omdb }`: + (rqut == 'tmdb' || (rqut == '*' && !mid && title && year) || apit == 'movie')? + (apit && apid)? + `https://api.themoviedb.org/3/${ apit }/${ apid }?api_key=${ api.tmdb }`: + (iid)? + `https://api.themoviedb.org/3/find/${ iid || mid || tid }?api_key=${ api.tmdb }&external_source=${ iid? 'imdb': mid? 'tmdb': 'tvdb' }_id`: + `https://api.themoviedb.org/3/search/${ apit }?api_key=${ api.tmdb }&query=${ encodeURI(title) }${ year? '&year=' + year: '' }`: + (rqut == 'tvdb' || (rqut == '*' && !tid && title) || (apid == tid))? + (tid)? + `https://api.tvmaze.com/shows/?thetvdb=${ tid }`: + (iid)? + `https://api.tvmaze.com/shows/?imdb=${ iid }`: + `https://api.tvmaze.com/search/shows?q=${ encodeURI(title) }`: + (title)? + (apit && year)? + `https://www.theimdbapi.org/api/find/${ apit }?title=${ encodeURI(title) }&year=${ year }`: + `https://www.theimdbapi.org/api/find/movie?title=${ encodeURI(title) }${ year? '&year=' + year: '' }`: + null; + + if(url === null) return null; + + let proxy = __CONFIG__.proxy, + cors = proxy.url, // if cors is requried and not uspported, proxy through this URL + headers = HandleProxyHeaders(proxy.headers, url); + + if(proxy.enabled && /(^http:\/\/)(?!localhost|127\.0\.0\.1(?:\/8)?|::1(?:\/128)?|:\d+)\b/i.test(url)) { + url = cors + .replace(/\{b(ase-?)?64-url\}/gi, btoa(url)) + .replace(/\{enc(ode)?-url\}/gi, encodeURIComponent(url)) + .replace(/\{(raw-)?url\}/gi, url); + + UTILS_TERMINAL.log({ proxy, url, headers }); + } + + UTILS_TERMINAL.log(`Searching for "${ title } (${ year })" in ${ type || apit }/${ rqut }${ proxy.enabled? '[PROXY]': '' } => ${ url }`); + + await(proxy.enabled? fetch(url, { mode: "cors", headers }): fetch(url)) + .then(response => response.text()) + .then(data => { + try { + if(data) + json = JSON.parse(data); + } catch(error) { + UTILS_TERMINAL.error(`Failed to parse JSON: "${ data }"`); + } + }) + .catch(error => { + throw error; + }); + + UTILS_TERMINAL.honor('Search results', { title, year, url, json }); + + /* DO NOT change to else-if, won't work with Sick Beard: { data: { results: ... } } */ + if('data' in json) + json = json.data; + if('results' in json) + json = json.results; + + if(json instanceof Array) { + let b = { release_date: '', year: '' }, + t = (s = "") => s.toLowerCase(), + c = (s = "") => t(s).replace(/\&/g, 'and').replace(UTF_16, ''), + k = (s = "") => { + + let r = [ + [/(?!^\s*)\b(show|series|a([st]|nd?|cross|fter|lthough)?|b(e(cause|fore|tween)|ut|y)|during|from|in(to)?|[io][fn]|[fn]?or|the|[st]o|through|under|with(out)?|yet)\b\s*/gi, ''], + // try replacing common words, e.g. Conjunctions, "Show," "Series," etc. + [/\^\s*|\s*$/g, ''], + [/\s+/g, '|'], + [/[\u2010-\u2015]/g, '-'], // fancy hyphen + [/[\u201a\u275f]/g, ','], // fancy comma + [/[\u2018\u2019\u201b\u275b\u275c`]/g, "'"], // fancy apostrophe (tilde from anime results by TMDb) + [/[\u201c-\u201f\u275d\u275e]/g, '"'], // fancy quotation marks + [/'(?=\B)|\B'/g, ''] + ]; + + for(let i = 0; i < r.length; i++) { + if(/^([\(\|\)]+)?$/.test(s)) return ""; + + s = s.replace(r[i][0], r[i][1]); + } + + return c(s); + }, + R = (s = "", S = "", n = !0) => { + let l = s.split(' ').length, L = S.split(' ').length, E, + score = 100 * (((S.match(E = RegExp(`\\b(${k(s)})\\b`, 'gi')) || [null]).length) / (L || 1)), + passing = __CONFIG__.UseLooseScore | 0; + + UTILS_TERMINAL.log(`\tQuick Match => "${ s }"/"${ S }" = ${ score }% (${ E })`); + score *= (l > L? (L||1)/l: L > l? (l||1)/L: 1); + UTILS_TERMINAL.log(`\tActual Match (${ passing }% to pass) ~> ... = ${ score }%`); + + return (S != '' && score >= passing) || (n? R(S, s, !n): n); + }, + en = /^(u[ks]-?|utf8-?)?en(glish)?$/i; + + // Find an exact match: Title (Year) | #IMDbID + let index, found, $data, lastscore; + for(index = 0, found = false, $data, lastscore = 0; (title && year) && index < json.length && !found; rerun |= 0b0100, index++) { + $data = json[index]; + + let altt = $data.alternativeTitles, + $alt = (altt && altt.length? altt.filter(v => t(v) == t(title))[0]: null); + + // Managers + if(manable) + // Medusa + if(__CONFIG__.usingMedusa && $data instanceof Array) + found = ((t($data[4]) == t(title) || $alt) && +year == +$data[5].slice(0, 4))? + $alt || $data: + found; + // Radarr & Sonarr + else if(__CONFIG__.usingRadarr || __CONFIG__.usingSonarr) + found = ((t($data.title) == t(title) || $alt) && +year == +$data.year)? + $alt || $data: + found; + // Sick Beard + else if(__CONFIG__.usingSickBeard) + found = ((t($data.name) == t(title) || $alt) && +year == parseInt($data.first_aired))? + $alt || $data: + found; + //api.tvmaze.com/ + else if(('externals' in ($data = $data.show || $data) || 'show' in $data) && $data.premiered) + found = (iid == $data.externals.imdb || t($data.name) == t(title) && year == $data.premiered.slice(0, 4))? + $data: + found; + //api.themoviedb.org/ \local + else if(('movie_results' in $data || 'tv_results' in $data || 'results' in $data) && $data.release_date) + found = (DATA => { + if(DATA.results) + if(rqut == 'tmdb') + DATA.movie_results = DATA.results; + else + DATA.tv_results = DATA.results; + + let i, f, o, l; + + for(i = 0, f = !1, o = DATA.movie_results, l = o.length | 0; i < l; i++) + f = (t(o.title) == t(title) && o.release_date.slice(0, 4) == year); + + for(i = (+f * l), o = (f? o: DATA.tv_results), l = (f? l: o.length | 0); i < l; i++) + f = (t(o.name) == t(title) && o.first_air_date.slice(0, 4) == year); + + return f? o: f = !!iid; + })($data); + //api.themoviedb.org/ \remote + else if(('original_name' in $data || 'original_title' in $data) && $data.release_date) + found = (tid == $data.id || (t($data.original_name || $data.original_title) == t(title) || t($data.name) == t(title)) && year == ($data || b).release_date.slice(0, 4))? + $data: + found; + //theimdbapi.org/ + else if($data.release_date) + found = (t($data.title) == t(title) && year == ($data.url || $data || b).release_date.slice(0, 4))? + $data: + found; + + UTILS_TERMINAL.log(`Strict Matching: ${ !!found }`, !!found? found: null); + } + + // Find a close match: Title + for(index = 0; title && index < json.length && (!found || lastscore > 0); rerun |= 0b0100, index++) { + $data = json[index]; + + let altt = $data.alternativeTitles, + $alt = (altt && altt.length? altt.filter(v => c(v) == c(title)): null); + + // Managers + if(manable) + // Medusa + if(__CONFIG__.usingMedusa && $data instanceof Array) + found = (c($data[4]) == c(title) || $alt)? + $alt || $data: + found; + // Radarr & Sonarr + if(__CONFIG__.usingRadarr || __CONFIG__.usingSonarr) + found = (c($data.title) == c(title) || $alt)? + $alt || $data: + found; + // Sick Beard + if(__CONFIG__.usingSickBeard) + found = (c($data.name) == c(title) || $alt)? + $alt || $data: + found; + //api.tvmaze.com/ + else if('externals' in ($data = $data.show || $data) || 'show' in $data) + found = + // ignore language barriers + (c($data.name) == c(title))? + $data: + // trust the api matching + ($data.score > lastscore)? + (lastscore = $data.score || $data.vote_count, $data): + found; + //api.themoviedb.org/ \local + else if('movie_results' in $data || 'tv_results' in $data || 'results' in $data) + found = (DATA => { + let i, f, o, l; + + if(DATA.results) + if(rqut == 'tmdb') + DATA.movie_results = DATA.results; + else + DATA.tv_results = DATA.results; + + for(i = 0, f = !1, o = DATA.movie_results, l = o.length | 0; i < l; i++) + f = (c(o.title) == c(title)); + + for(i = (+f * l), o = (f? o: DATA.tv_results), l = (f? l: o.length | 0); i < l; i++) + f = (c(o.name) == c(title)); + + return f? o: f; + })($data); + //api.themoviedb.org/ \remote + else if('original_name' in $data || 'original_title' in $data || 'name' in $data) + found = (c($data.original_name || $data.original_title || $data.name) == c(title))? + $data: + found; + //theimdbapi.org/ + else if(en.test($data.language)) + found = (c($data.title) == c(title))? + $data: + found; + + UTILS_TERMINAL.log(`Title Matching: ${ !!found }`, !!found? found: null); + } + + // Find an OK match (Loose Searching): Title ~ Title + // The examples below are correct + // GOOD, found: VRV's "Bakemonogatari" vs. TVDb's "Monogatari Series" + // /\b(monogatari)\b/i.test('bakemonogatari') === true + // this is what this option was designed for + // OK, found: "The Title of This is Bad" vs. "The Title of This is OK" (this is semi-errornous) + // /\b(title|this|bad)\b/i.test('title this ok') === true + // this may be a possible match, but it may also be an error: 'title' and 'this' + // the user's defined threshold is used in this case (above 65% would match these two items) + // BAD, not found: "Gun Show Showdown" vs. "Gundarr" + // /\b(gun|showdown)\b/i.test('gundarr') === false + // this should not match; the '\b' (border between \w and \W) keeps them from matching + for(index = 0; __CONFIG__.UseLoose && title && index < json.length && (!found || lastscore > 0); rerun |= 0b0010, index++) { + $data = json[index]; + + let altt = $data.alternativeTitles, + $alt = (altt && altt.length? altt.filter(v => R(v, title)): null); + + // Managers + if(manable) + // Medusa + if(__CONFIG__.usingMedusa && $data instanceof Array) + found = (R($data[4], title) || $alt)? + $alt || $data: + found; + // Radarr & Sonarr + if(__CONFIG__.usingRadarr || __CONFIG__.usingSonarr) + found = (R($data.name || $data.title, title) || $alt)? + $alt || $data: + found; + // Sick Beard + if(__CONFIG__.usingSickBeard) + found = (R($data.name, title) || $alt)? + $alt || $data: + found; + //api.tvmaze.com/ + else if('externals' in ($data = $data.show || $data) || 'show' in $data) + found = + // ignore language barriers + (R($data.name, title) || UTILS_TERMINAL.log('Matching:', [$data.name, title], R($data.name, title)))? + $data: + // trust the api matching + ($data.score > lastscore)? + (lastscore = $data.score, $data): + found; + //api.themoviedb.org/ \local + else if('movie_results' in $data || 'tv_results' in $data) + found = (DATA => { + let i, f, o, l; + + for(i = 0, f = !1, o = DATA.movie_results, l = o.length | 0; i < l; i++) + f = R(o.title, title); + + for(i = (+f * l), o = (f? o: DATA.tv_results), l = (f? l: o.length | 0); i < l; i++) + f = R(o.name, title); + + return f? o: f; + })($data); + //api.themoviedb.org/ \remote + else if('original_name' in $data || 'original_title' in $data) + found = (R($data.original_name, title) || R($data.original_title, title) || R($data.name, title))? + $data: + found; + //theimdbapi.org/ + else if(en.test($data.language)) + found = (R($data.title, title))? + $data: + found; + + UTILS_TERMINAL.log(`Loose Matching: ${ !!found }`, !!found? found: null); + } + + json = found; + } + + if((json === undefined || json === null || json === false) && !(rerun & 0b0001)) + return UTILS_TERMINAL.warn(`Trying to find "${ title }" again (as "${ (alttitle || title) }")`), rerun |= 0b0001, json = Identify({ title: (alttitle || title), year: YEAR, type, IMDbID, TMDbID, TVDbID, APIType, APIID, meta, rerun }); + else if((json === undefined || json === null)) + json = { IMDbID, TMDbID, TVDbID }; + + let ei = 'tt', + mr = 'movie_results', + tr = 'tv_results'; + + json = json && mr in json? json[mr].length > json[tr].length? json[mr]: json[tr]: json; + + if(json instanceof Array && (!__CONFIG__.usingMedusa? true: (__CONFIG__.usingSonarr || __CONFIG__.usingOmbi || __CONFIG__.usingSickBeard))) + json = json[0]; + + if(!json) + json = { IMDbID, TMDbID, TVDbID }; + + // Ombi, Medusa, Radarr and Sonarr + if(manable) + data = ( + (__CONFIG__.usingMedusa && !(__CONFIG__.usingSonarr || __CONFIG__.usingOmbi || __CONFIG__.usingSickBeard))? + { + imdb: iid || ei, + tmdb: mid | 0, + tvdb: tid || json[3] || (json[8]? json[8][1]: 0), + title: json.title || title, + year: +(json.year || year) + }: + { + imdb: iid || json.imdbId || ei, + tmdb: mid || json.tmdbId || json.theMovieDbId | 0, + tvdb: tid || json.tvdbId || json.theTvDbId | 0, + title: json.title || title, + year: +(json.year || year) + } + ); + //api.tvmaze.com/ + else if('externals' in (json = json.show || json)) + data = { + imdb: iid || json.externals.imdb || ei, + tmdb: mid || json.externals.themoviedb | 0, + tvdb: tid || json.externals.thetvdb | 0, + title: json.name || title, + year: ((json.premiered || json.first_aired_date || year) + '').slice(0, 4) + }; + //api.themoviedb.org/ + else if('imdb_id' in (json = mr in json? json[mr].length > json[tr].length? json[mr]: json[tr]: json) || 'original_name' in json || 'original_title' in json) + data = { + imdb: iid || json.imdb_id || ei, + tmdb: mid || json.id | 0, + tvdb: tid || json.tvdb | 0, + title: json.title || json.name || title, + year: ((json.release_date || json.first_air_date || year) + '').slice(0, 4) + }; + //omdbapi.com/ + else if('imdbID' in json) + data = { + imdb: iid || json.imdbID || ei, + tmdb: mid || json.tmdbID | 0, + tvdb: tid || json.tvdbID | 0, + title: json.Title || json.Name || title, + year: json.Year || year + }; + //theapache64.com/movie_db/ + else if('data' in json) + data = { + imdb: iid || json.data.imdb_id || ei, + tmdb: mid || json.data.tmdb_id | 0, + tvdb: tid || json.data.tvdb_id | 0, + title: json.data.name || json.data.title || title, + year: json.data.year || year + }; + //theimdbapi.org/ + else if('imdb' in json) + data = { + imdb: iid || json.imdb || ei, + tmdb: mid || json.id | 0, + tvdb: tid || json.tvdb | 0, + title, + year + }; + // given by the requesting service + else + data = { + imdb: iid || ei, + tmdb: mid | 0, + tvdb: tid | 0, + title, + year + }; + + year = +((data.year + '').slice(0, 4)) || 0; + data.year = year; + + let best = { title, year, data, type, rqut, score: json.score | 0 }; + + UTILS_TERMINAL.log('Best match:', url, { best, json }); + + if(best.data.imdb == ei && best.data.tmdb == 0 && best.data.tvdb == 0) + return UTILS_TERMINAL.log(`No information was found for "${ title } (${ year })"`), {}; + + save(savename, data); // e.g. "Coco (0)" on Netflix before correction / no repeat searches + save(savename = `${title} (${year}).${rqut}`.toLowerCase(), data); // e.g. "Coco (2017)" on Netflix after correction / no repeat searches + save(`${title}.${rqut}`.toLowerCase(), year); + + UTILS_TERMINAL.log(`Saved as "${ savename }"`, data); + + rerun |= 0b00001; + + return data; + } + + function Request_CouchPotato(options) { + // TODO: this does not work anymore! + if(!options.IMDbID) + return new Notification( + 'warning', + 'Stopped adding to CouchPotato: No IMDb ID' + ); + + chrome.runtime.sendMessage( + { + type: 'VIEW_COUCHPOTATO', + url: `${ __CONFIG__.couchpotatoURL }/media.get`, + IMDbID: options.IMDbID, + TMDbID: options.TMDbID, + TVDbID: options.TVDbID, + basicAuth: __CONFIG__.couchpotatoBasicAuth, + }, + response => { + let movieExists = response.success; + if(response.error) { + return new Notification( + 'warning', + 'CouchPotato request failed (see your console)' + ) || + (!response.silent && UTILS_TERMINAL.error('Error viewing CouchPotato: ' + String(response.error))); + } + if(!movieExists) { + __Request_CouchPotato__(options); + return; + } + new Notification( 'warning', - 'CouchPotato request failed (see your console)' - ) || - (!response.silent && terminal.error('Error viewing CouchPotato: ' + String(response.error))); + `Movie already exists in CouchPotato (status: ${response.status})` + ); } - if (!movieExists) { - pushCouchPotatoRequest(options); - return; + ); + } + + // Movies/TV Shows + function Request_Ombi(options) { + new Notification('info', `Sending "${ options.title }" to Ombi`, 3000); + + if((!options.IMDbID && !options.TMDbID) && !options.TVDbID) { + return new Notification( + 'warning', + 'Stopped adding to Ombi: No content ID' + ); + } + + let contentType = (/movies?|film/i.test(options.type)? 'movie': 'tv'); + + chrome.runtime.sendMessage({ + type: 'PUSH_OMBI', + url: `${ __CONFIG__.ombiURL }api/v1/Request/${ contentType }`, + token: __CONFIG__.ombiToken, + title: options.title, + year: options.year, + imdbId: options.IMDbID, + tmdbId: options.TMDbID, + tvdbId: options.TVDbID, + contentType, + }, + response => { + UTILS_TERMINAL.log('Pushing to Ombi', response); + + if(response && response.error) { + return new Notification('warning', `Could not add "${ options.title }" to Ombi: ${ response.error }`) || + (!response.silent && UTILS_TERMINAL.error('Error adding to Ombi: ' + String(response.error), response.location, response.debug)); + } else if(response && response.success) { + let title = options.title.replace(/\&/g, 'and').replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-{2,}/g, '-').toLowerCase(), + { IMDbID, TMDbID, TVDbID } = options; + + CAUGHT.bump({ IMDbID, TMDbID, TVDbID }); + + UTILS_TERMINAL.honor('Successfully pushed', options); + new Notification('update', `Added "${ options.title }" to Ombi`, 7000, () => window.open(__CONFIG__.ombiURL, '_blank')); + } else { + new Notification('warning', `Could not add "${ options.title }" to Ombi: Unknown Error`) || + (!response.silent && UTILS_TERMINAL.error('Error adding to Ombi: ' + String(response))); + } + } + ); + } + + // Movies/TV Shows + function __Request_CouchPotato__(options) { + new Notification('info', `Sending "${ options.title }" to CouchPotato`, 3000); + + chrome.runtime.sendMessage( + { + type: 'PUSH_COUCHPOTATO', + url: `${ __CONFIG__.couchpotatoURL }/movie.add`, + IMDbID: options.IMDbID, + TMDbID: options.TMDbID, + TVDbID: options.TVDbID, + basicAuth: __CONFIG__.couchpotatoBasicAuth, + }, + response => { + UTILS_TERMINAL.log('Pushing to CouchPotato', response); + + if(response.error) { + return new Notification( + 'warning', + `Could not add "${ options.title }" to CouchPotato (see your console)` + ) || + (!response.silent && UTILS_TERMINAL.error('Error adding to CouchPotato: ' + String(response.error), response.location, response.debug)); + } + if(response.success) { + let { IMDbID, TMDbID, TVDbID } = options; + + CAUGHT.bump({ IMDbID, TMDbID, TVDbID }); + + UTILS_TERMINAL.honor('Successfully pushed', options); + new Notification('update', `Added "${ options.title }" to CouchPotato`); + } else { + new Notification('warning', `Could not add "${ options.title }" to CouchPotato`); + } } - new Notification( + ); + } + + // Movies + function Request_Watcher(options) { + new Notification('info', `Sending "${ options.title }" to Watcher`, 3000); + + if(!options.IMDbID && !options.TMDbID) { + return new Notification( 'warning', - `Movie is already in CouchPotato (status: ${response.status})` + 'Stopped adding to Watcher: No IMDb/TMDb ID' ); } - ); -} -// Movies/TV Shows -function pushOmbiRequest(options) { - new Notification('info', `Adding "${ options.title }" to Ombi`, 3000); - - if ((!options.IMDbID && !options.TMDbID) && !options.TVDbID) { - return new Notification( - 'warning', - 'Stopped adding to Ombi: No content ID' - ); - } - - let contentType = (/movies?|film/i.test(options.type)? 'movie': 'tv'); - - chrome.runtime.sendMessage({ - type: 'ADD_OMBI', - url: `${ config.ombiURL }api/v1/Request/${ contentType }`, - token: config.ombiToken, - title: options.title, - year: options.year, - imdbId: options.IMDbID, - tmdbId: options.TMDbID, - tvdbId: options.TVDbID, - contentType, - }, - response => { - terminal.log('Pushing to Ombi', response); - - if (response && response.error) { - return new Notification('warning', `Could not add "${ options.title }" to Ombi: ${ response.error }`) || - (!response.silent && terminal.error('Error adding to Ombi: ' + String(response.error), response.location, response.debug)); - } else if (response && response.success) { - let title = options.title.replace(/\&/g, 'and').replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-{2,}/g, '-').toLowerCase(); - - terminal.log('Successfully pushed', options); - new Notification('update', `Added "${ options.title }" to Ombi`, 7000, () => window.open(config.ombiURL, '_blank')); - } else { - new Notification('warning', `Could not add "${ options.title }" to Ombi: Unknown Error`) || - (!response.silent && terminal.error('Error adding to Ombi: ' + String(response))); - } - } - ); -} + chrome.runtime.sendMessage({ + type: 'PUSH_WATCHER', + url: `${ __CONFIG__.watcherURL }api/`, + token: __CONFIG__.watcherToken, + StoragePath: __CONFIG__.watcherStoragePath, + basicAuth: __CONFIG__.watcherBasicAuth, + title: options.title, + year: options.year, + imdbId: options.IMDbID, + tmdbId: options.TMDbID, + }, + response => { + UTILS_TERMINAL.log('Pushing to Watcher', response); + + if(response && response.error) { + return new Notification('warning', `Could not add "${ options.title }" to Watcher: ${ response.error }`) || + (!response.silent && UTILS_TERMINAL.error('Error adding to Watcher: ' + String(response.error), response.location, response.debug)); + } else if(response && (response.success || (response.response + "") == "true")) { + let title = options.title.replace(/\&/g, 'and').replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-{2,}/g, '-').toLowerCase(), + TMDbID = options.TMDbID || response.tmdbId, + IMDbID = options.IMDbID || response.imdbId; + + CAUGHT.bump({ IMDbID, TMDbID }); + + UTILS_TERMINAL.honor('Successfully pushed', options); + new Notification('update', `Added "${ options.title }" to Watcher`, 7000, () => window.open(`${__CONFIG__.watcherURL}library/status${TMDbID? `#${title}-${TMDbID}`: '' }`, '_blank')); + } else { + new Notification('warning', `Could not add "${ options.title }" to Watcher: Unknown Error`) || + (!response.silent && UTILS_TERMINAL.error('Error adding to Watcher: ' + String(response))); + } + } + ); + } -// Movies/TV Shows -function pushCouchPotatoRequest(options) { - new Notification('info', `Adding "${ options.title }" to CouchPotato`, 3000); - - chrome.runtime.sendMessage( - { - type: 'ADD_COUCHPOTATO', - url: `${ config.couchpotatoURL }/movie.add`, - IMDbID: options.IMDbID, - TMDbID: options.TMDbID, - TVDbID: options.TVDbID, - basicAuth: config.couchpotatoBasicAuth, - }, - response => { - terminal.log('Pushing to CouchPotato', response); + // Movies + function Request_Radarr(options, prompted) { + if(!options.IMDbID && !options.TMDbID) + return (!prompted)? new Notification( + 'warning', + 'Stopped adding to Radarr: No IMDb/TMDb ID' + ): null; + + let PromptValues = {}, + { PromptQuality, PromptLocation } = __CONFIG__; + + if(!prompted && (PromptQuality || PromptLocation)) + return new Prompt('modify', options, refined => Request_Radarr(refined, true)); + + if(PromptQuality && +options.quality > 0) + PromptValues.QualityID = +options.quality; + if(PromptLocation && options.location) + PromptValues.StoragePath = JSON.parse(__CONFIG__.radarrStoragePaths).map(item => item.id == options.location? item: null).filter(n => n)[0].path.replace(/\\/g, '\\\\'); + + new Notification('info', `Sending "${ options.title }" to Radarr`, 3000); + + chrome.runtime.sendMessage({ + type: 'PUSH_RADARR', + url: `${ __CONFIG__.radarrURL }api/movie/`, + token: __CONFIG__.radarrToken, + StoragePath: __CONFIG__.radarrStoragePath, + QualityID: __CONFIG__.radarrQualityProfileId, + basicAuth: __CONFIG__.radarrBasicAuth, + title: options.title, + year: options.year, + imdbId: options.IMDbID, + tmdbId: options.TMDbID, + ...PromptValues + }, + response => { + UTILS_TERMINAL.log('Pushing to Radarr', response); + + if(response && response.error) { + return new Notification('warning', `Could not add "${ options.title }" to Radarr: ${ response.error }`) || + (!response.silent && UTILS_TERMINAL.error('Error adding to Radarr: ' + String(response.error), response.location, response.debug)); + } else if(response && response.success) { + let title = options.title.replace(/\&/g, 'and').replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-{2,}/g, '-').toLowerCase(), + TMDbID = options.TMDbID || response.tmdbId, + IMDbID = options.IMDbID || response.imdbId; + + CAUGHT.bump({ IMDbID, TMDbID }); + + UTILS_TERMINAL.honor('Successfully pushed', options); + new Notification('update', `Added "${ options.title }" to Radarr`, 7000, () => window.open(`${__CONFIG__.radarrURL}${TMDbID? `movies/${title}-${TMDbID}`: '' }`, '_blank')); + } else { + new Notification('warning', `Could not add "${ options.title }" to Radarr: Unknown Error`) || + (!response.silent && UTILS_TERMINAL.error('Error adding to Radarr: ' + String(response))); + } + } + ); + } - if (response.error) { - return new Notification( - 'warning', - `Could not add "${ options.title }" to CouchPotato (see your console)` - ) || - (!response.silent && terminal.error('Error adding to CouchPotato: ' + String(response.error), response.location, response.debug)); + // TV Shows + function Request_Sonarr(options, prompted) { + if(!options.TVDbID) + return (!prompted)? new Notification( + 'warning', + 'Stopped adding to Sonarr: No TVDb ID' + ): null; + + let PromptValues = {}, + { PromptQuality, PromptLocation } = __CONFIG__; + + if(!prompted && (PromptQuality || PromptLocation)) + return new Prompt('modify', options, refined => Request_Sonarr(refined, true)); + + if(PromptQuality && +options.quality > 0) + PromptValues.QualityID = +options.quality; + if(PromptLocation && options.location) + PromptValues.StoragePath = JSON.parse(__CONFIG__.sonarrStoragePaths).map(item => item.id == options.location? item: null).filter(n => n)[0].path.replace(/\\/g, '\\\\'); + + new Notification('info', `Sending "${ options.title }" to Sonarr`, 3000); + + chrome.runtime.sendMessage({ + type: 'PUSH_SONARR', + url: `${ __CONFIG__.sonarrURL }api/series/`, + token: __CONFIG__.sonarrToken, + StoragePath: __CONFIG__.sonarrStoragePath, + QualityID: __CONFIG__.sonarrQualityProfileId, + basicAuth: __CONFIG__.sonarrBasicAuth, + title: options.title, + year: options.year, + tvdbId: options.TVDbID, + ...PromptValues + }, + response => { + UTILS_TERMINAL.log('Pushing to Sonarr', response); + + if(response && response.error) { + return new Notification('warning', `Could not add "${ options.title }" to Sonarr: ${ response.error }`) || + (!response.silent && UTILS_TERMINAL.error('Error adding to Sonarr: ' + String(response.error), response.location, response.debug)); + } else if(response && response.success) { + let title = options.title.replace(/\&/g, 'and').replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-{2,}/g, '-').toLowerCase(), + TVDbID = options.TVDbID || response.tvdbId; + + CAUGHT.bump({ TVDbID }); + + UTILS_TERMINAL.honor('Successfully pushed', options); + new Notification('update', `Added "${ options.title }" to Sonarr`, 7000, () => window.open(`${__CONFIG__.sonarrURL}series/${title}`, '_blank')); + } else { + new Notification('warning', `Could not add "${ options.title }" to Sonarr: Unknown Error`) || + (!response.silent && UTILS_TERMINAL.error('Error adding to Sonarr: ' + String(response))); + } } - if (response.success) { - terminal.log('Successfully pushed', options); - new Notification('update', `Added "${ options.title }" to CouchPotato`); - } else { - new Notification('warning', `Could not add "${ options.title }" to CouchPotato`); + ); + } + + // TV Shows + function Request_Medusa(options, prompted) { + if(!options.TVDbID) + return (!prompted)? new Notification( + 'warning', + 'Stopped adding to Medusa: No TVDb ID' + ): null; + + let PromptValues = {}, + { PromptQuality, PromptLocation } = __CONFIG__; + + if(!prompted && (PromptQuality || PromptLocation)) + return new Prompt('modify', options, refined => Request_Medusa(refined, true)); + + if(PromptQuality && +options.quality > 0) + PromptValues.QualityID = +options.quality; + if(PromptLocation && options.location) + PromptValues.StoragePath = JSON.parse(__CONFIG__.medusaStoragePaths).map(item => item.id == options.location? item: null).filter(n => n)[0].path.replace(/\\/g, '\\\\'); + + new Notification('info', `Sending "${ options.title }" to Medusa`, 3000); + + chrome.runtime.sendMessage({ + type: 'PUSH_MEDUSA', + url: `${ __CONFIG__.medusaURL }api/v2/series`, + root: `${ __CONFIG__.medusaURL }api/v2/`, + token: __CONFIG__.medusaToken, + StoragePath: __CONFIG__.medusaStoragePath, + QualityID: __CONFIG__.medusaQualityProfileId, + basicAuth: __CONFIG__.medusaBasicAuth, + title: options.title, + year: options.year, + tvdbId: options.TVDbID, + ...PromptValues + }, + response => { + UTILS_TERMINAL.log('Pushing to Medusa', response); + + if(response && response.error) { + return new Notification('warning', `Could not add "${ options.title }" to Medusa: ${ response.error }`) || + (!response.silent && UTILS_TERMINAL.error('Error adding to Medusa: ' + String(response.error), response.location, response.debug)); + } else if(response && response.success) { + let title = options.title.replace(/\&/g, 'and').replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-{2,}/g, '-').toLowerCase(), + TVDbID = options.TVDbID || response.tvdbId; + + CAUGHT.bump({ TVDbID }); + + UTILS_TERMINAL.honor('Successfully pushed', options); + new Notification('update', `Added "${ options.title }" to Medusa`, 7000, () => window.open(`${__CONFIG__.medusaURL}home/displayShow?indexername=tvdb&seriesid=${options.TVDbID}`, '_blank')); + } else { + new Notification('warning', `Could not add "${ options.title }" to Medusa: Unknown Error`) || + (!response.silent && UTILS_TERMINAL.error('Error adding to Medusa: ' + String(response))); + } } + ); + } + + // TV Shows + function Request_SickBeard(options, prompted) { + if(!options.TVDbID) + return (!prompted)? new Notification( + 'warning', + 'Stopped adding to Sick Beard: No TVDb ID' + ): null; + + let PromptValues = {}, + { PromptQuality, PromptLocation } = __CONFIG__; + + if(!prompted && (PromptQuality || PromptLocation)) + return new Prompt('modify', options, refined => Request_SickBeard(refined, true)); + + if(PromptQuality && +options.quality > 0) + PromptValues.QualityID = +options.quality; + if(PromptLocation && +options.location >= 0) + PromptValues.StoragePath = JSON.parse(__CONFIG__.sickBeardStoragePaths)[+options.location].path.replace(/\\/g, '\\\\'); + + new Notification('info', `Sending "${ options.title }" to Sick Beard`, 3000); + + chrome.runtime.sendMessage({ + type: 'PUSH_SICKBEARD', + url: `${ __CONFIG__.sickBeardURL }api/${ __CONFIG__.sickBeardToken }/`, + token: __CONFIG__.sickBeardToken, + StoragePath: __CONFIG__.sickBeardStoragePath, + QualityID: __CONFIG__.sickBeardQualityProfileId, + basicAuth: __CONFIG__.sickBeardBasicAuth, + title: options.title, + year: options.year, + tvdbId: options.TVDbID, + exists: !!~JSON.parse(__CONFIG__.__caught).tvdb.indexOf(options.TVDbID), + ...PromptValues + }, + response => { + UTILS_TERMINAL.log('Pushing to Sick Beard', response); + + if(response && response.error) { + return new Notification('warning', `Could not add "${ options.title }" to Sick Beard: ${ response.error }`) || + (!response.silent && UTILS_TERMINAL.error('Error adding to Sick Beard: ' + String(response.error), response.location, response.debug)); + } else if(response && response.success) { + let title = options.title.replace(/\&/g, 'and').replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-{2,}/g, '-').toLowerCase(), + TVDbID = options.TVDbID || response.tvdbId; + + CAUGHT.bump({ TVDbID }); + + UTILS_TERMINAL.honor('Successfully pushed', options); + new Notification('update', `Added "${ options.title }" to Sick Beard`, 7000, () => window.open(`${__CONFIG__.sickBeardURL}home/displayShow?show=${ TVDbID }`, '_blank')); + } else { + new Notification('warning', `Could not add "${ options.title }" to Sick Beard: Unknown Error`) || + (!response.silent && UTILS_TERMINAL.error('Error adding to Sick Beard: ' + String(response))); + } + } + ); + } + + // make the button + // ( PERSISTENT, { HEADER_CLASSES } ) + let MASTER_BUTTON; + function RenderButton(persistent, headers = {}) { + let existingButtons = document.queryBy('.web-to-plex-button'), + firstButton = existingButtons.first; + + if(existingButtons.length && !persistent) + [].slice.call(existingButtons).forEach(button => button.remove()); + else if(persistent && firstButton !== null && firstButton !== undefined) + return firstButton; + + let ThemeClasses = JSON.parse(__CONFIG__.__theme), + HeaderClasses = []; + + // Theme(s) + if(!ThemeClasses.length) + ThemeClasses = ''; + else + ThemeClasses = '.' + ThemeClasses.join('.'); + + // Header(s) + for(let header in headers) + if(headers[header]) + HeaderClasses.push( header ); + + if(!HeaderClasses.length) + HeaderClasses = ''; + else + HeaderClasses = '.' + HeaderClasses.join('.'); + + // + + document.body.appendChild(button); + + return MASTER_BUTTON = button; + } + + function UpdateButton(button, action, title, options = {}) { + if(!button) + return /* Rare, but happens: especially on failed download links sent*/; + + let multiple = (action == 'multiple' || options instanceof Array), + element = button.querySelector('.w2p-action, .list-action'), + delimeter = '', + ty = 'Item', txt = 'title', hov = 'tooltip', + em = /^(tt|0)?$/i, + tv = /tv[\s-]?|shows?|series/i; + + if(!element) { + element = button; + button = element.parentElement; + }; + + Update('SEARCH_FOR', { ...options, button }); + + /* Handle a list of items */ + if(multiple) { + options = [].slice.call(options); + + let saved_options = [], // a list of successful searches (not on Plex) + len = options.length, + s = (len == 1? '': 's'), + t = []; + + for(let index = 0; index < len; index++) { + let option = options[index]; + + // Skip empty entries + if(!option || !option.type || !option.title) + continue; + + // Skip queued entries + if( + !!~CAUGHT.imdb.indexOf(option.IMDbID) || + !!~CAUGHT.tmdb.indexOf(option.TMDbID) || + !!~CAUGHT.tvdb.indexOf(option.TVDbID) + ) + continue; + + // the action should be an array + // we'll give the button a list of links to engage, so make it snappy! + let url = `#${ option.IMDbID || 'tt' }-${ option.TMDbID | 0 }-${ option.TVDbID | 0 }`; + + /* Failed */ + if(/#tt-0-0/i.test(url)) + continue; + + saved_options.push(option); + t.push(option.title); + } + + t = t.join(', '); + t = t.length > 24? t.slice(0, 21).replace(/\W+$/, '') + '...': t; + + element.ON_CLICK = e => { + e.preventDefault(); + + let self = e.target, tv = /tv[\s-]?|shows?|series/i, fail = 0, + options = JSON.parse(decode(button.getAttribute('saved_options'))); + + for(let index = 0, length = options.length, option; index < length; index++) { + option = options[index]; + + try { + if(__CONFIG__.usingOmbi) + Request_Ombi(option, true); + else if(__CONFIG__.usingWatcher && !tv.test(option.type)) + Request_Watcher(option, true); + else if(__CONFIG__.usingRadarr && !tv.test(option.type)) + Request_Radarr(option, true); + else if(__CONFIG__.usingCouchPotato && !tv.test(option.type)) + Request_CouchPotato(option, true); + else if(__CONFIG__.usingSonarr && tv.test(option.type)) + Request_Sonarr(option, true); + else if(__CONFIG__.usingMedusa && tv.test(option.type)) + Request_Medusa(option, true); + else if(__CONFIG__.usingSickBeard && tv.test(option.type)) + Request_SickBeard(option, true); + + button.classList.replace('wtp--download', 'wtp--queued'); + } catch(error) { + UTILS_TERMINAL.error(`Failed to get "${ option.title }" (Error #${ ++fail })`) + } + } + NOTIFIED = false; + + if(fail) + new Notification('error', `Failed to grab ${ fail } item${fail==1?'':'s'}`); + }; + + button.setAttribute('saved_options', encode(JSON.stringify(saved_options))); + element.addEventListener('click', e => (AUTO_GRAB.ENABLED && AUTO_GRAB.LIMIT > options.length)? element.ON_CLICK(e): new Prompt('select', options, o => { button.setAttribute('saved_options', encode(JSON.stringify(o))); element.ON_CLICK(e) })); + + element.setAttribute(hov, `Grab ${len} new item${s}: ${ t }`); + button.classList.add(saved_options.length || len? 'wtp--download': 'wtp--error'); + } else { + /* Handle a single item */ + + if(!options || !options.type || !options.title) + return; + + let empty = (em.test(options.IMDbID) && em.test(options.TMDbID) && em.test(options.TVDbID)), + nice_title = `${options.title.toCaps()}${options.year? ` (${options.year})`: ''}`; + + if(options) { + ty = (/^(cine(ma)?|films?|movies?|theat[re]{2})$/i.test(options.type)? 'Movie': 'TV Show'); + txt = options.txt || txt; + hov = options.hov || hov; + } + + if(action == 'found') { + element.href = Request_PlexURL(__CONFIG__.server.id, options.key); + element.setAttribute(hov, `Watch "${options.title} (${options.year})" on Plex`); + button.classList.add('wtp--found'); + + new Notification('success', `Watch "${ nice_title }"`, 7000, e => element.click(e)); + } else if(action == 'downloader' || options.remote) { + + switch(options.remote) { + /* Vumoo & GoStream */ + case 'plex': + case 'oload': + case 'consistent': + let href = options.href, path = ''; + + if(__CONFIG__.usingOmbi) { + path = ''; + } else if(__CONFIG__.usingWatcher && !tv.test(options.type)) { + path = ''; + } else if(__CONFIG__.usingRadarr && !tv.test(options.type)) { + path = __CONFIG__.radarrStoragePath; + } else if(__CONFIG__.usingSonarr && tv.test(options.type)) { + path = __CONFIG__.sonarrStoragePath; + } else if(__CONFIG__.usingMedusa && tv.test(options.type)) { + path = __CONFIG__.medusaStoragePath; + } else if(__CONFIG__.usingSickBeard && tv.test(options.type)) { + path = __CONFIG__.sickBeardStoragePath; + } else if(__CONFIG__.usingCouchPotato) { + path = ''; + } + + element.href = `#${ options.IMDbID || 'tt' }-${ options.TMDbID | 0 }-${ options.TVDbID | 0 }`; + + button.classList.remove('wtp--queued'); + button.classList.add('wtp--download'); + + element.removeEventListener('click', element.ON_CLICK); + element.addEventListener('click', element.ON_DOWNLOAD = e => { + e.preventDefault(); + + Update('DOWNLOAD_FILE', { ...options, button, href, path }); + new Notification('update', 'Opening prompt (may take a while)...'); + }); + + element.setAttribute(hov, `Download "${ nice_title }" | ${ty}`); + Update('SAVE_AS', { ...options, button, href, path }); + new Notification('update', `"${ nice_title }" can be downloaded`, 7000, e => element.click(e)); + return; + + + /* Default & Error */ + default: + let url = `#${ options.IMDbID || 'tt' }-${ options.TMDbID | 0 }-${ options.TVDbID | 0 }`; + + /* Failed */ + if(/#tt-0-0/i.test(url)) + return UpdateButton(button, 'notfound', title, options); + + element.href = url; + button.classList.add('wtp--download'); + element.addEventListener('click', element.ON_CLICK = e => { + e.preventDefault(); + try { + if(__CONFIG__.usingOmbi) + Request_Ombi(options); + else if(__CONFIG__.usingWatcher && !tv.test(options.type)) + Request_Watcher(options); + else if(__CONFIG__.usingRadarr && !tv.test(options.type)) + Request_Radarr(options); + else if(__CONFIG__.usingCouchPotato && !tv.test(options.type)) + Request_CouchPotato(options); + else if(__CONFIG__.usingSonarr && tv.test(options.type)) + Request_Sonarr(options); + else if(__CONFIG__.usingMedusa && tv.test(options.type)) + Request_Medusa(options); + else if(__CONFIG__.usingSickBeard && tv.test(options.type)) + Request_SickBeard(options); + + button.classList.replace('wtp--download', 'wtp--queued'); + } catch(error) { + throw error; + } + + }); + } + NOTIFIED = false; + + element.setAttribute(hov, `Add "${ nice_title }" | ${ty}`); + element.style.removeProperty('display'); + } else if(action == 'notfound' || action == 'error' || empty) { + element.removeAttribute('href'); + + empty = !(options && options.title); + + if(empty) + element.setAttribute(hov, `${ty || 'Item'} not found`); + else + element.setAttribute(hov, `"${ nice_title }" was not found`); + + button.classList.remove('wtp--found'); + button.classList.add('wtp--error'); + } + + if((action == 'downloader') && (!!~CAUGHT.imdb.indexOf(options.IMDbID) || !!~CAUGHT.tmdb.indexOf(options.TMDbID) || !!~CAUGHT.tvdb.indexOf(options.TVDbID))) { + element.setAttribute(hov, `Modify "${ nice_title }" | ${ty}`); + + button.classList.remove('wtp--found'); + button.classList.add('wtp--queued'); + } + + element.id = options? `${options.IMDbID || 'tt'}-${options.TMDbID | 0}-${options.TVDbID | 0}`: 'tt-0-0'; } - ); -} + } -// Movies -function pushWatcherRequest(options) { - new Notification('info', `Adding "${ options.title }" to Watcher`, 3000); - - if (!options.IMDbID && !options.TMDbID) { - return new Notification( - 'warning', - 'Stopped adding to Watcher: No IMDb/TMDb ID' - ); - } - - chrome.runtime.sendMessage({ - type: 'ADD_WATCHER', - url: `${ config.watcherURL }api/`, - token: config.watcherToken, - StoragePath: config.watcherStoragePath, - basicAuth: config.watcherBasicAuth, - title: options.title, - year: options.year, - imdbId: options.IMDbID, - tmdbId: options.TMDbID, - }, - response => { - terminal.log('Pushing to Watcher', response); - - if (response && response.error) { - return new Notification('warning', `Could not add "${ options.title }" to Watcher: ${ response.error }`) || - (!response.silent && terminal.error('Error adding to Watcher: ' + String(response.error), response.location, response.debug)); - } else if (response && (response.success || (response.response + "") == "true")) { - let title = options.title.replace(/\&/g, 'and').replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-{2,}/g, '-').toLowerCase(), - TMDbID = options.TMDbID || response.tmdbId; - - terminal.log('Successfully pushed', options); - new Notification('update', `Added "${ options.title }" to Watcher`, 7000, () => window.open(`${config.watcherURL}library/status${TMDbID? `#${title}-${TMDbID}`: '' }`, '_blank')); - } else { - new Notification('warning', `Could not add "${ options.title }" to Watcher: Unknown Error`) || - (!response.silent && terminal.error('Error adding to Watcher: ' + String(response))); - } - } - ); -} + // Find media on Plex + async function FindMediaItems(options, button) { + if(!(options && options.length && button)) + return; -// Movies -function pushRadarrRequest(options) { - new Notification('info', `Adding "${ options.title }" to Radarr`, 3000); - - if (!options.IMDbID && !options.TMDbID) { - return new Notification( - 'warning', - 'Stopped adding to Radarr: No IMDb/TMDb ID' - ); - } - - chrome.runtime.sendMessage({ - type: 'ADD_RADARR', - url: `${ config.radarrURL }api/movie/`, - token: config.radarrToken, - StoragePath: config.radarrStoragePath, - QualityID: config.radarrQualityProfileId, - basicAuth: config.radarrBasicAuth, - title: options.title, - year: options.year, - imdbId: options.IMDbID, - tmdbId: options.TMDbID, - }, - response => { - terminal.log('Pushing to Radarr', response); - - if (response && response.error) { - return new Notification('warning', `Could not add "${ options.title }" to Radarr: ${ response.error }`) || - (!response.silent && terminal.error('Error adding to Radarr: ' + String(response.error), response.location, response.debug)); - } else if (response && response.success) { - let title = options.title.replace(/\&/g, 'and').replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-{2,}/g, '-').toLowerCase(), - TMDbID = options.TMDbID || response.tmdbId; - - terminal.log('Successfully pushed', options); - new Notification('update', `Added "${ options.title }" to Radarr`, 7000, () => window.open(`${config.radarrURL}${TMDbID? `movies/${title}-${TMDbID}`: '' }`, '_blank')); - } else { - new Notification('warning', `Could not add "${ options.title }" to Radarr: Unknown Error`) || - (!response.silent && terminal.error('Error adding to Radarr: ' + String(response))); - } - } - ); -} + let results = [], + length = options.length, + queries = (FindMediaItems.queries = FindMediaItems.queries || {}); -// TV Shows -function pushSonarrRequest(options) { - new Notification('info', `Adding "${ options.title }" to Sonarr`, 3000); - - if (!options.TVDbID) { - return new Notification( - 'warning', - 'Stopped adding to Sonarr: No TVDb ID' - ); - } - - chrome.runtime.sendMessage({ - type: 'ADD_SONARR', - url: `${ config.sonarrURL }api/series/`, - token: config.sonarrToken, - StoragePath: config.sonarrStoragePath, - QualityID: config.sonarrQualityProfileId, - basicAuth: config.sonarrBasicAuth, - title: options.title, - year: options.year, - tvdbId: options.TVDbID, - }, - response => { - terminal.log('Pushing to Sonarr', response); - - if (response && response.error) { - return new Notification('warning', `Could not add "${ options.title }" to Sonarr: ${ response.error }`) || - (!response.silent && terminal.error('Error adding to Sonarr: ' + String(response.error), response.location, response.debug)); - } else if (response && response.success) { - let title = options.title.replace(/\&/g, 'and').replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-{2,}/g, '-').toLowerCase(); - - terminal.log('Successfully pushed', options); - new Notification('update', `Added "${ options.title }" to Sonarr`, 7000, () => window.open(`${config.sonarrURL}series/${title}`, '_blank')); - } else { - new Notification('warning', `Could not add "${ options.title }" to Sonarr: Unknown Error`) || - (!response.silent && terminal.error('Error adding to Sonarr: ' + String(response))); - } - } - ); -} + FindMediaItems.OPTIONS = options; -// make the button -function renderPlexButton(persistent) { - let existingButtons = document.querySelectorAll('.web-to-plex-button'), - firstButton = existingButtons[0]; - - if (existingButtons.length && !persistent) - [].slice.call(existingButtons).forEach(button => button.remove()); - else if(persistent && firstButton !== null && firstButton !== undefined) - return firstButton; - - // - - document.body.appendChild(button); - - return button; -} + let query = JSON.stringify(options); -function modifyPlexButton(button, action, title, options = {}) { - let multiple = (action == 'multiple' || options instanceof Array), - element = button.querySelector('.w2p-action, .list-action'), - delimeter = '', - ty = 'Item', txt = 'title', hov = 'tooltip', - em = /^(tt|0)?$/i, - tv = /tv[\s-]?|shows?|series/i; - - if(!element) { - element = button; - button = element.parentElement; - }; - - sendUpdate('SEARCH_FOR', { ...options, button }); - - /* Handle a list of items */ - if(multiple) { - options = [].slice.call(options); - - let saved_options = [], // a list of successful searches (not on Plex) - len = options.length, - s = (len == 1? '': 's'), - t = []; - - for(let index = 0; index < len; index++) { - let option = options[index]; - - // Skip empty entries - if(!option || !option.type || !option.title) continue; - - // the action should be an array - // we'll give the button a list of links to engage, so make it snappy! - let url = `#${ option.imdb || 'tt' }-${ option.tmdb | 0 }-${ option.tvdb | 0 }`; - - /* Failed */ - if(/#tt-0-0/i.test(url)) - continue; - - saved_options.push(option); - t.push(option.title); - } - - t = t.join(', '); - t = t.length > 24? t.slice(0, 21).replace(/\W+$/, '') + '...': t; - - element.ON_CLICK = e => { - e.preventDefault(); - - let self = e.target, tv = /tv[\s-]?|shows?|series/i, fail = 0, - options = JSON.parse(atob(button.getAttribute('saved_options'))); - - for(let index = 0, length = options.length, option; index < length; index++) { - option = options[index]; - - try { - if(config.ombiURL) - pushOmbiRequest(option); - else if (config.watcherURL && !tv.test(option.type)) - pushWatcherRequest(option); - else if (config.radarrURL && !tv.test(option.type)) - pushRadarrRequest(option); - else if (config.sonarrURL && tv.test(option.type)) - pushSonarrRequest(option); - else if(config.couchpotatoURL && tv.test(option.type)) - $pushAddToCouchpotato(option); - } catch(error) { - terminal.error(`Failed to get "${ option.title }" (Error #${ ++fail })`) - } - } - - if (fail) - new Notification('error', `Failed to grab ${ fail } item${fail==1?'':'s'}`); - }; - - button.setAttribute('saved_options', btoa(JSON.stringify(saved_options))); - element.addEventListener('click', e => (AUTO_GRAB.ENABLED && AUTO_GRAB.LIMIT > options.length)? element.ON_CLICK(e): new Prompt('select', options, o => { button.setAttribute('saved_options', btoa(JSON.stringify(o))); element.ON_CLICK(e) })); - - element.setAttribute(hov, `Grab ${len} new item${s}: ${ t }`); - button.classList.add(saved_options.length || len? 'wtp--download': 'wtp--error'); - } else { - /* Handle a single item */ - - if(!options || !options.type || !options.title) return; - - let empty = (em.test(options.IMDbID) && em.test(options.TMDbID) && em.test(options.TVDbID)), - nice_title = `${options.title.toCaps()}${options.year? ` (${options.year})`: ''}`; - - if(options) { - ty = (options.type == 'movie'? 'Movie': 'TV Show'); - txt = options.txt || txt; - hov = options.hov || hov; - } - - if (action == 'found') { - element.href = getPlexMediaURL(config.server.id, options.key); - element.setAttribute(hov, `Watch "${options.title} (${options.year})" on Plex`); - button.classList.add('wtp--found'); - } else if (action == 'downloader' || options.remote) { - - switch(options.remote) { - /* GoStream */ - case 'oload': - let href = options.href, path = ''; - - if (config.ombiURL) { - path = ''; - } else if (config.watcherURL && !tv.test(options.type)) { - path = ''; - } else if (config.radarrURL && !tv.test(options.type)) { - path = config.radarrStoragePath; - } else if (config.sonarrURL && tv.test(options.type)) { - path = config.sonarrStoragePath; - } else if(config.couchpotatoURL && tv.test(options.type)) { - path = ''; - } - - element.href = `#${ options.IMDbID || 'tt' }-${ options.TMDbID | 0 }-${ options.TVDbID | 0 }`; - button.classList.add('wtp--download'); - element.removeEventListener('click', element.ON_CLICK); - element.addEventListener('click', element.ON_DOWNLOAD = e => { - e.preventDefault(); - - sendUpdate('DOWNLOAD_FILE', { ...options, button, href, path }); - new Notification('update', 'Opening prompt (may take a while)...'); - }); - - element.setAttribute(hov, `Download "${ nice_title }" | ${ty}`); - sendUpdate('SAVE_AS', { ...options, button, href, path }); - new Notification('update', `"${ nice_title }" can be downloaded`, 7000, e => element.click(e)); - return; - - - /* Default & Error */ - default: - let url = `#${ options.IMDbID || 'tt' }-${ options.TMDbID | 0 }-${ options.TVDbID | 0 }`; - - /* Failed */ - if(/#tt-0-0/i.test(url)) - return modifyPlexButton(button, 'notfound', title, options); - - element.href = url; - button.classList.add('wtp--download'); - element.addEventListener('click', element.ON_CLICK = e => { - e.preventDefault(); - if (config.ombiURL) { - pushOmbiRequest(options); - } else if (config.watcherURL && !tv.test(options.type)) { - pushWatcherRequest(options); - } else if (config.radarrURL && !tv.test(options.type)) { - pushRadarrRequest(options); - } else if (config.sonarrURL && tv.test(options.type)) { - pushSonarrRequest(options); - } else if(config.couchpotatoURL && tv.test(options.type)) { - $pushAddToCouchpotato(options); - } - }); - } - - element.setAttribute(hov, `Add "${ nice_title }" | ${ty}`); - element.style.removeProperty('display'); - } else if (action == 'notfound' || action == 'error' || empty) { - element.removeAttribute('href'); - - empty = !(options && options.title); - - if(empty) - element.setAttribute(hov, `${ty || 'Item'} not found`); - else - element.setAttribute(hov, `"${ nice_title }" was not found`); - - button.classList.remove('wtp--found'); - button.classList.add('wtp--error'); - } - - element.id = options? `${options.IMDbID || 'tt'}-${options.TMDbID | 0}-${options.TVDbID | 0}`: 'tt-0-0'; - } -} + query = (queries[query] = queries[query] || {}); -async function squabblePlex(options, button) { - if(!(options && options.length && button)) - return; - - let results = [], - length = options.length; - - squabblePlex.OPTIONS = options; - - new Notification('info', `Processing ${ length } item${ 's'[+(length === 1)] || '' }...`); - - for(let index = 0, option, opt; index < length; index++) { - let { IMDbID, TMDbID, TVDbID } = (option = await options[index]); - - opt = { name: option.title, title: option.title, year: option.year, image: options.image, type: option.type, imdb: IMDbID, IMDbID, tmdb: TMDbID, TMDbID, tvdb: TVDbID, TVDbID }; - - try { - await getPlexMediaRequest(option) - .then(async({ found, key }) => { - if (found) { - // ignore found items, we only want new items - } else { - option.field = 'original_title'; - - return await getPlexMediaRequest(option) - .then(({ found, key }) => { - if (found) { - // ignore found items, we only want new items - } else { - let available = (config.ombiURL || config.watcherURL || config.radarrURL || config.sonarrURL || config.couchpotatoURL), - action = (available ? 'downloader' : 'notfound'), - title = available ? - 'Not on Plex (download available)': - 'Not on Plex (download not available)'; - - results.push({ ...opt, found: false, status: action }); - } - }); - } - }) - } catch(error) { - terminal.error('Request to Plex failed: ' + String(error)); - // new Notification('error', 'Failed to query item #' + (index + 1)); - } - } - - results = results.filter(v => v.status == 'downloader'); - - let img = furnish('img', { title: 'Add to Plex It!', src: IMG_URL.p48, onclick: event => {let frame = document.querySelector('#plexit-bookmarklet-frame'); frame.src = frame.src.replace(/(#plexit:.*)?$/, '#plexit:' + event.target.parentElement.getAttribute('data'))} }), - po, pi = furnish('li#plexit.list-item', { data: btoa(JSON.stringify(results)) }, img), - op = document.querySelector('#wtp-plexit'); - - if(po = button.querySelector('#plexit')) - po.remove(); - button.querySelector('ul').insertBefore(pi, op); - - let multiple = results.length, - items = multiple == 1? 'item': 'items'; - - new Notification('update', `Done. ${ multiple } new ${ items } can be grabbed`, 7000, (event, target = button.querySelector('.list-action')) => target.click({ ...event, target })); - - if (multiple) - modifyPlexButton(button, 'multiple', `Download ${ multiple } ${ items }`, results); -} + if(query.running === true) + return; + else if(query.results) { + let { results, multiple, items } = query; -function findPlexMedia(options) { - if(!(options && options.title)) - return; - - let { IMDbID, TMDbID, TVDbID } = options; - - TMDbID = +TMDbID; - TVDbID = +TVDbID; - - let opt = { name: options.title, year: options.year, image: options.image || IMG_URL.nil, type: options.type, imdb: IMDbID, IMDbID, tmdb: TMDbID, TMDbID, tvdb: TVDbID, TVDbID }, - op = document.querySelector('#wtp-plexit'), - img = (options.image)? - furnish('div', { tooltip: 'Add to Plex It!', style: `background: url(${ IMG_URL.p16 }) top right/60% no-repeat, #0004 url(${ opt.image }) center/contain no-repeat; height: 48px; width: 34px;`, draggable: true, onclick: event => {let frame = document.querySelector('#plexit-bookmarklet-frame'); frame.src = frame.src.replace(/(#plexit:.*)?$/, '#plexit:' + event.target.parentElement.getAttribute('data'))} }): - furnish('img', { title: 'Add to Plex It!', src: IMG_URL.p48, onclick: event => {let frame = document.querySelector('#plexit-bookmarklet-frame'); frame.src = frame.src.replace(/(#plexit:.*)?$/, '#plexit:' + event.target.parentElement.getAttribute('data'))} }); - - findPlexMedia.OPTIONS = options; - - try { - getPlexMediaRequest(options) - .then(({ found, key }) => { - if (found) { - modifyPlexButton(options.button, 'found', 'On Plex', { ...options, key }); - opt = { ...opt, url: options.button.href, found: true, status: 'found' }; - - let po, pi = furnish('li#plexit.list-item', { data: btoa(JSON.stringify(opt)) }, img); - - if(po = options.button.querySelector('#plexit')) - po.remove(); - options.button.querySelector('ul').insertBefore(pi, op); - } else { - options.field = 'original_title'; - - return getPlexMediaRequest(options) - .then(({ found, key }) => { - if (found) { - modifyPlexButton(options.button, 'found', 'On Plex', { ...options, key }); - opt = { ...opt, url: options.button.href, found: true, status: 'found' }; - - let po, pi = furnish('li#plexit.list-item', { data: btoa(JSON.stringify(opt)) }, img); - - if(po = options.button.querySelector('#plexit')) - po.remove(); - options.button.querySelector('ul').insertBefore(pi, op); - } else { - let available = (config.ombiURL || config.watcherURL || config.radarrURL || config.sonarrURL || config.couchpotatoURL), - action = (available ? 'downloader' : 'notfound'), - title = available ? - 'Not on Plex (download available)': - 'Not on Plex (download not available)'; - - modifyPlexButton(options.button, action, title, options); - opt = { ...opt, found: false, status: action }; - - let po, pi = furnish('li#plexit.list-item', { data: btoa(JSON.stringify(opt)) }, img); - - if(po = options.button.querySelector('#plexit')) - po.remove(); - if(!!~[].slice.call(options.button.querySelector('ul').children).indexOf(op)) - options.button.querySelector('ul').insertBefore(pi, op); - } - }); - } - }) - } catch(error) { - return modifyPlexButton( - options.button, - 'error', - 'Request to Plex Media Server failed', - options - ), - terminal.error(`Request to Plex failed: ${ String(error) }`); - // new Notification('Failed to communicate with Plex'); - } -} + new Notification('update', `Welcome back. ${ multiple } new ${ items } can be grabbed`, 7000, (event, target = button.querySelector('.list-action')) => target.click({ ...event, target })); -function getPlexMediaRequest(options) { - if(!(config.plexURL && config.plexToken) || config.DO_NOT_USE) - return new Promise((resolve, reject) => resolve({ found: false, key: null })); - - return new Promise((resolve, reject) => { - chrome.runtime.sendMessage({ - type: 'SEARCH_PLEX', - options, - serverConfig: config.server - }, - response => - (response && response.error)? - reject(response.error): - (!response)? - reject(new Error('Unknown error')): - resolve(response) - ); - }); + if(multiple) + UpdateButton(button, 'multiple', `Download ${ multiple } ${ items }`, results); + + return; + } + + query.running = true; + + new Notification('info', `Processing ${ length } item${ 's'[+(length === 1)] || '' }...`); + + for(let index = 0, option, opt; index < length; index++) { + let { IMDbID, TMDbID, TVDbID } = (option = await options[index]); + + opt = { name: option.title, title: option.title, year: option.year, image: options.image, type: option.type, imdb: IMDbID, IMDbID, tmdb: TMDbID, TMDbID, tvdb: TVDbID, TVDbID }; + + try { + await Request_Plex(option) + .then(async({ found, key }) => { + if(found) { + // ignore found items, we only want new items + } else { + option.field = 'original_title'; + + return await Request_Plex(option) + .then(({ found, key }) => { + if(found) { + // ignore found items, we only want new items + } else { + let available = (__CONFIG__.usingOmbi || __CONFIG__.usingWatcher || __CONFIG__.usingRadarr || __CONFIG__.usingSonarr || __CONFIG__.usingMedusa || __CONFIG__.usingSickBeard || __CONFIG__.usingCouchPotato), + action = (available ? 'downloader' : 'notfound'), + title = available ? + 'Not on Plex (download available)': + 'Not on Plex (download not available)'; + + results.push({ ...opt, found: false, status: action }); + } + }); + } + }) + } catch(error) { + UTILS_TERMINAL.error('Request to Plex failed: ' + String(error)); + // new Notification('error', 'Failed to query item #' + (index + 1)); + } + } + + results = results.filter(v => v.status == 'downloader'); + + let img = furnish('img', { title: 'Add to Plex It!', src: IMG_URL.plexit_icon_48, onmouseup: event => {let frame = document.querySelector('#plexit-bookmarklet-frame'); frame.src = frame.src.replace(/(#plexit:.*)?$/, '#plexit:' + event.target.parentElement.getAttribute('data'))} }), + po, pi = furnish('li#plexit.list-item', { data: encode(JSON.stringify(results)) }, img), + op = document.querySelector('#wtp-plexit'); + + if(po = button.querySelector('#plexit')) + po.remove(); + try { + button.querySelector('ul').insertBefore(pi, op); + } catch(e) { /* Don't do anything */ } + + let multiple = results.length, + items = multiple == 1? 'item': 'items'; + + new Notification('update', `Done. ${ multiple } new ${ items } can be grabbed`, 7000, (event, target = button.querySelector('.list-action')) => target.click({ ...event, target })); + + query.running = false; + query.results = results; + query.multiple = multiple; + query.items = items; + + if(multiple) + UpdateButton(button, 'multiple', `Download ${ multiple } ${ items }`, results); + } + + async function FindMediaItem(options) { + if(!(options && options.title)) + return; + + let { IMDbID, TMDbID, TVDbID } = options; + + TMDbID = +TMDbID; + TVDbID = +TVDbID; + + let opt = { name: options.title, year: options.year, image: options.image || IMG_URL.nil, type: options.type, imdb: IMDbID, IMDbID, tmdb: TMDbID, TMDbID, tvdb: TVDbID, TVDbID }, + op = document.querySelector('#wtp-plexit'), + img = (options.image)? + furnish('div', { tooltip: 'Add to Plex It!', style: `background: url(${ IMG_URL.plexit_icon_16 }) top right/60% no-repeat, #0004 url(${ opt.image }) center/contain no-repeat; height: 48px; width: 34px;`, draggable: true, onmouseup: event => {let frame = document.querySelector('#plexit-bookmarklet-frame'); frame.src = frame.src.replace(/(#plexit:.*)?$/, '#plexit:' + event.target.parentElement.getAttribute('data'))} }): + furnish('img', { title: 'Add to Plex It!', src: IMG_URL.plexit_icon_48, onmouseup: event => {let frame = document.querySelector('#plexit-bookmarklet-frame'); frame.src = frame.src.replace(/(#plexit:.*)?$/, '#plexit:' + event.target.parentElement.getAttribute('data'))} }); + + FindMediaItem.OPTIONS = options; + + try { + return Request_Plex(options) + .then(({ found, key }) => { + if(found) { + UpdateButton(options.button, 'found', 'On Plex', { ...options, key }); + opt = { ...opt, url: options.button.href, found: true, status: 'found' }; + + let po, pi = furnish('li#plexit.list-item', { data: encode(JSON.stringify(opt)) }, img); + + if(po = options.button.querySelector('#plexit')) + po.remove(); + try { + options.button.querySelector('ul').insertBefore(pi, op); + } catch(e) { /* Don't do anything */ } + } else { + options.field = 'original_title'; + + return Request_Plex(options) + .then(({ found, key }) => { + if(found) { + UpdateButton(options.button, 'found', 'On Plex', { ...options, key }); + opt = { ...opt, url: options.button.href, found: true, status: 'found' }; + + let po, pi = furnish('li#plexit.list-item', { data: encode(JSON.stringify(opt)) }, img); + + if(po = options.button.querySelector('#plexit')) + po.remove(); + try { + options.button.querySelector('ul').insertBefore(pi, op); + } catch(e) { /* Don't do anything */ } + } else { + let available = (__CONFIG__.usingOmbi || __CONFIG__.usingWatcher || __CONFIG__.usingRadarr || __CONFIG__.usingSonarr || __CONFIG__.usingMedusa || __CONFIG__.usingSickBeard || __CONFIG__.usingCouchPotato), + action = (available ? 'downloader' : 'notfound'), + title = available ? + 'Not on Plex (download available)': + 'Not on Plex (download not available)'; + + UpdateButton(options.button, action, title, options); + opt = { ...opt, found: false, status: action }; + + let po, pi = furnish('li#plexit.list-item', { data: encode(JSON.stringify(opt)) }, img); + + if(po = options.button.querySelector('#plexit')) + po.remove(); + if(!!~[].slice.call(options.button.querySelector('ul').children).indexOf(op)) + try { + options.button.querySelector('ul').insertBefore(pi, op); + } catch(e) { /* Don't do anything */ } + } + return found; + }); + } + return found; + }) + } catch(error) { + return UpdateButton( + options.button, + 'error', + 'Request to Plex Media Server failed', + options + ), + UTILS_TERMINAL.error(`Request to Plex failed: ${ String(error) }`), + false; + // new Notification('Failed to communicate with Plex'); + } + } + + function Request_Plex(options) { + if(!(__CONFIG__.plexURL && __CONFIG__.plexToken) || __CONFIG__.IGNORE_PLEX) + return new Promise((resolve, reject) => resolve({ found: false, key: null })); + + return new Promise((resolve, reject) => { + // Sanitize the object + options = JSON.parse( JSON.stringify(options) ); + + chrome.runtime.sendMessage({ + type: 'SEARCH_PLEX', + options, + serverConfig: __CONFIG__.server + }, + response => + (response && response.error)? + reject(response.error): + (!response)? + reject(new Error('Unknown error')): + resolve(response) + ); + }); + } + + function Request_PlexURL(PlexUIID, key) { + return __CONFIG__.plexURL.replace(RegExp(`\/(${ __CONFIG__.server.id })?$`), `/web#!/server/` + PlexUIID) + `/details?key=${encodeURIComponent( key )}`; + } + + /* Listen for events */ + chrome.runtime.onMessage.addListener(async(request, sender) => { + UTILS_TERMINAL.log(`Listener event [${ request.instance_type }#${ request[request.instance_type.toLowerCase()] }]:`, request); + + let data = request.data, + LOCATION = `${ request.name || 'anonymous' } @ instance ${ request.instance }`, + PARSING_ERROR = `Can't parse missing information. ${ LOCATION }`, + BUTTON_ERROR = `The button failed to render. ${ LOCATION }`, + EMPTY_REQUEST = `The given request is empty. ${ LOCATION }`; + + if(!data) + return UTILS_TERMINAL.warn(EMPTY_REQUEST); + let button = RenderButton(); + + if(!button) + return UTILS_TERMINAL.warn(BUTTON_ERROR); + button.classList.remove('sleeper'); + + switch(request.type) { + case 'POPULATE': + + if(data instanceof Array) { + for(let index = 0, length = data.length, item; index < length; index++) + if(!(item = data[index]) || !item.type) + data.splice(index, 1, null); + + data = data.filter(value => value !== null && value !== undefined); + + for(let index = 0, length = data.length, item; index < length; index++) { + let { image, type, title, year, IMDbID, TMDbID, TVDbID } = (item = data[index]); + + if(!item.title || !item.type) + continue; + + let Db = await Identify(item); + + IMDbID = IMDbID || Db.imdb || 'tt'; + TMDbID = TMDbID || Db.tmdb || 0; + TVDbID = TVDbID || Db.tvdb || 0; + + title = title || Db.title; + year = +(year || Db.year || 0); + + data.splice(index, 1, { type, title, year, image, button, IMDbID, TMDbID, TVDbID }); + } + + if(!data.length) + return UTILS_TERMINAL.error(PARSING_ERROR); + else + FindMediaItems(data, button); + } else { + if(!data || !data.title || !data.type) + return UTILS_TERMINAL.error(PARSING_ERROR); + + let { image, type, title, year, IMDbID, TMDbID, TVDbID } = data; + let Db = await Identify(data); + + IMDbID = IMDbID || Db.imdb || 'tt'; + TMDbID = TMDbID || Db.tmdb || 0; + TVDbID = TVDbID || Db.tvdb || 0; + + title = title || Db.title; + year = +(year || Db.year || 0); + + let found = await FindMediaItem({ type, title, year, image, button, IMDbID, TMDbID, TVDbID }); + Update('FOUND', { ...request, found }, true); + } + return true; + + case 'INITIALIZE': + init && init(); + return true; + + case 'NO_RENDER': + document.queryBy('.web-to-plex-button').map(e => e.remove()); + return true; + + default: + // UTILS_TERMINAL.warn(`Unknown event [${ request.type }]`); + return false; + } + }); + + /* Listen for Window events - from iframes, etc. */ + top.addEventListener('message', async request => { + try { + request = request.data; + + switch(request.type) { + case 'SEND_VIDEO_LINK': + let options = { ...FindMediaItem.OPTIONS, href: request.href, remote: request.from }; + + UTILS_TERMINAL.log(`Download Event [${ options.remote }]:`, options); + + UpdateButton(MASTER_BUTTON, 'downloader', 'Download', options); + return true; + + case 'NOTIFICATION': + let { state, text, timeout = 7000, callback = () => {}, requiresClick = true } = request.data; + new Notification(state, text, timeout, callback, requiresClick); + return true; + + case 'PERMISSION': + let { data } = request; + + if(typeof data.allowed == 'boolean') { + ALLOWED = data.allowed; + PERMISS = data.allotted; + + await ParsedOptions(); + + (init && !RUNNING? (init(), RUNNING = true): RUNNING = false); + } else { + UTILS_TERMINAL.warn('Permission Request:', data); + new Prompt('permission', data); + } + return true; + + default: + // UTILS_TERMINAL.warn(`Unknown event [${ request.type }]`); + return false; + } + } catch(error) { + new Notification('error', `Unable to use downloader: ${ String(error) }`); + throw error + } + }); + + // create the sleeping button + wait(() => document.readyState === 'complete', () => RenderButton(null, { sleeper: true })); + +})(new Date); + +/* Helpers */ + +function wait(on, then) { + if(on && ((on instanceof Function && on()) || true)) + then && then(); + else + setTimeout(() => wait(on, then), 50); } -function getPlexMediaURL(PlexUIID, key) { - return config.plexURL.replace(RegExp(`\/(${ config.server.id })?$`), `/web#!/server/` + PlexUIID) + `/details?key=${encodeURIComponent( key )}`; +// the custom "on location change" event +function watchlocationchange(subject) { + let locationchangecallbacks = watchlocationchange.locationchangecallbacks; + + watchlocationchange[subject] = watchlocationchange[subject] || location[subject]; + + if(watchlocationchange[subject] != location[subject]) { + let from = watchlocationchange[subject], + to = location[subject], + properties = { from, to }, + sign = code => (code + '').replace(/\W+/g, '').toLowerCase(); + + watchlocationchange[subject] = location[subject]; + + for(let index = 0, length = locationchangecallbacks.length, callback, exists, signature; length > 0 && index < length; index++) { + callback = locationchangecallbacks[index]; + exists = locationchangecallbacks.exists[signature = sign(callback)]; + + let event = new Event('locationchange', { bubbles: true }); + + if(!exists && typeof callback == 'function') { + /* The eventlistener does not exist */ + window.addEventListener('beforeunload', event => { + event.preventDefault(false); + callback({ event, ...properties }); + }); + } else { + /* The eventlistener already exists */ + callback({ event, ...properties }); + } + + open(to, '_self'); + } + } } +watchlocationchange.locationchangecallbacks = watchlocationchange.locationchangecallbacks || []; +watchlocationchange.locationchangecallbacks.exists = watchlocationchange.locationchangecallbacks.exists || {}; + +if(!('onlocationchange' in window)) + Object.defineProperty(window, 'onlocationchange', { + set: callback => { + if(typeof callback != 'function') + return null; -/* Listen for Plugin events */ -chrome.runtime.onMessage.addListener(async(request, sender) => { - terminal.log(`Plugin event [${ request.plugin }]:`, request); - - switch(request.type) { - case 'POPULATE': - let button = renderPlexButton(), - data = request.data, - PARSING_ERROR = `Can't parse missing information. ${ request.name } @ instance ${ request.instance }`, - BUTTON_ERROR = `The button failed to render. ${ request.name } @ instance ${ request.instance }`; - - if(!button) - return terminal.warn(BUTTON_ERROR); - - if(data instanceof Array) { - for(let index = 0, length = data.length, item; index < length; index++) - if(!(item = data[index]) || !item.type) - data.splice(index, 1, null); - - data = data.filter(value => value !== null && value !== undefined); - - for(let index = 0, length = data.length, item; index < length; index++) { - let { image, type, title, year, IMDbID, TMDbID, TVDbID } = (item = data[index]); - - if(!item.title || !item.type) - continue; - - let Db = await getIDs(item); - - IMDbID = IMDbID || Db.imdb || 'tt'; - TMDbID = TMDbID || Db.tmdb || 0; - TVDbID = TVDbID || Db.tvdb || 0; - - title = title || Db.title; - year = +(year || Db.year || 0); - - data.splice(index, 1, { type, title, year, image, button, IMDbID, TMDbID, TVDbID }); - } - - if(!data.length) - return terminal.error(PARSING_ERROR); - else - squabblePlex(data, button); - } else { - if(!data.title || !data.type) - return terminal.error(PARSING_ERROR); - - let { image, type, title, year, IMDbID, TMDbID, TVDbID } = data; - let Db = await getIDs(data); - - IMDbID = IMDbID || Db.imdb || 'tt'; - TMDbID = TMDbID || Db.tmdb || 0; - TVDbID = TVDbID || Db.tvdb || 0; - - title = title || Db.title; - year = +(year || Db.year || 0); - - findPlexMedia({ type, title, year, image, button, IMDbID, TMDbID, TVDbID }); - } - return true; - - default: -// terminal.warn(`Unknown event [${ request.type }]`); - return false; - } -}); - -/* Listen for Window events - from iframes, etc. */ -top.addEventListener('message', request => { - try { - request = request.data; - - switch(request.type) { - case 'SEND_VIDEO_LINK': - let options = { ...findPlexMedia.OPTIONS, href: request.href, remote: request.from }; - - modifyPlexButton(options.button, 'downloader', 'Download', options); - return true; - - default: - // terminal.warn(`Unknown event [${ request.type }]`); - return false; - } - } catch(error) { - new Notification('error', `Unable to use downloader: ${ String(error) }`); - throw error - } -}); - -String.prototype.toCaps = function toCaps(all) { - /** Titling Caplitalization - * Articles: a, an, & the - * Conjunctions: and, but, for, nor, or, so, & yet - * Prepositions: across, after, although, at, because, before, between, by, during, from, if, in, into, of, on, to, through, under, with, & without - */ - let array = this.toLowerCase(), - titles = /(?!^|(?:an?|the)\s+)\b(a([st]|nd?|cross|fter|lthough)?|b(e(cause|fore|tween)|ut|y)|during|from|in(to)?|[io][fn]|[fn]?or|the|[st]o|through|under|with(out)?|yet)(?!\s*$)\b/gi, - cap_exceptions = /([\|\"\(]\s*[a-z]|[\:\.\!\?]\s+[a-z]|(?:^\b|[^\'\-\+]\b)[^aeiouy\d\W]+\b)/gi, // Punctuation exceptions, e.g. "And not I" - all_exceptions = /\b((?:ww)?(?:m+[dclxvi]*|d+[clxvi]*|c+[lxvi]*|l+[xvi]*|x+[vi]*|v+i*|i+))\b/gi, // Roman Numberals - cam_exceptions = /\b((?:mr?s|[sdjm]r|mx)|(?:adm|cm?dr?|chf|c[op][lmr]|cpt|gen|lt|mjr|sgt)|doc|hon|prof)\./gi; // Titles (Most Common?) - - array = array.split(/\s+/); - - let index, length, string, word; - for(index = 0, length = array.length, string = [], word; index < length; index++) { - word = array[index]; - - if(word) - string.push( word[0].toUpperCase() + word.slice(1, word.length) ); - } - - string = string.join(' '); - - if(!all) - string = string - .replace(titles, ($0, $1, $$, $_) => $1.toLowerCase()) - .replace(cap_exceptions, ($0, $1, $$, $_) => $1.toUpperCase()) - .replace(all_exceptions, ($0, $1, $$, $_) => $1.toUpperCase()) - .replace(cam_exceptions, ($0, $1, $$, $_) => $0[0].toUpperCase() + $0.slice(1, $0.length).toLowerCase()); - - return string; + let signature = (callback + '').replace(/\W+/g, '').toLowerCase(); + + if(!watchlocationchange.locationchangecallbacks.exists[signature]) { + watchlocationchange.locationchangecallbacks.exists[signature] = true; + + return watchlocationchange.locationchangecallbacks.push(callback); + } + return null; + }, + get: () => watchlocationchange.locationchangecallbacks + }); + +watchlocationchange.onlocationchangeinterval = watchlocationchange.onlocationchangeinterval || setInterval(() => watchlocationchange('href'), 1000); +// at least 1s is needed to properly fire the event ._. + +String.prototype.toCaps = String.prototype.toCaps || function toCaps(all) { + /** Titling Caplitalization + * Articles: a, an, & the + * Conjunctions: and, but, for, nor, or, so, & yet + * Prepositions: across, after, although, at, because, before, between, by, during, from, if, in, into, of, on, to, through, under, with, & without + */ + let array = this.toLowerCase(), + titles = /(?!^|(?:an?|the)\s+)\b(a([st]|nd?|cross|fter|lthough)?|b(e(cause|fore|tween)?|ut|y)|during|from|in(to)?|[io][fn]|[fn]?or|the|[st]o|through|under|with(out)?|yet)(?!\s*$)\b/gi, + cap_exceptions = /([\|\"\(]\s*[a-z]|[\:\.\!\?]\s+[a-z]|(?:^\b|[^\'\-\+]\b)[^aeiouy\d\W]+\b)/gi, // Punctuation exceptions, e.g. "And not I" + all_exceptions = /\b((?:ww)?(?:m{1,4}(?:c?d(?:c{0,3}(?:x?l(?:x{0,3}(?:i?vi{0,3})?)?)?)?)?|c?d(?:c{0,3}(?:x?l(?:x{0,3}(?:i?vi{0,3})?)?)?)?|c{1,3}(?:x?l(?:x{0,3}(?:i?vi{0,3})?)?)?|x?l(?:x{0,3}(?:i?vi{0,3})?)?|x{1,3}(?:i?vi{0,3})?|i?vi{0,3}|i{1,3}))\b/gi, // Roman Numberals + cam_exceptions = /\b((?:mr?s|[sdjm]r|mx)|(?:adm|cm?dr?|chf|c[op][lmr]|cpt|gen|lt|mjr|sgt)|doc|hon|prof)(?:\.|\b)/gi, // Titles (Most Common?) + low_exceptions = /'([\w]+)/gi; // Apostrphe cases + + array = array.split(/\s+/); + + let index, length, string, word; + for(index = 0, length = array.length, string = [], word; index < length; index++) { + word = array[index]; + + if(word) + string.push( word[0].toUpperCase() + word.slice(1, word.length) ); + } + + string = string.join(' '); + + if(!all) + string = string + .replace(titles, ($0, $1, $$, $_) => $1.toLowerCase()) + .replace(all_exceptions, ($0, $1, $$, $_) => $1.toUpperCase()) + .replace(cap_exceptions, ($0, $1, $$, $_) => $1.toUpperCase()) + .replace(low_exceptions, ($0, $1, $$, $_) => $0.toLowerCase()) + .replace(cam_exceptions, ($0, $1, $$, $_) => $1[0].toUpperCase() + $1.slice(1, $1.length).toLowerCase() + '.'); + + return string; }; (function(parent) { @@ -1720,7 +2590,7 @@ String.prototype.toCaps = function toCaps(all) { let index = 0; // the order given is the order handled document.queryBy("div:last-child, div:nth-child(2), div:first-child") - .forEach((element, index, array) => element.innerHTML = index + 1); + .forEach((element, index, array) => element.innerHTML = index + 1); // output w/sortBySelector:
3
@@ -1732,149 +2602,169 @@ String.prototype.toCaps = function toCaps(all) {
2
3
*/ - parent.queryBy = function queryBy(selectors, container = parent) { - // Helpers - let copy = array => [].slice.call(array), - query = (SELECTORS, CONTAINER = container) => CONTAINER.querySelectorAll(SELECTORS); - - // Get rid of enclosing syntaxes: [...] and (...) - let regexp = /(\([^\(\)]+?\)|\[[^\[\]]+?\])/g, - pulled = [], - media = [], - index, length; - - // The index shouldn't be longer than the length of the selector's string - // Keep this to prevent infinite loops - for(index = 0, length = selectors.length; index++ < length && regexp.test(selectors);) - selectors = selectors.replace(regexp, ($0, $1, $$, $_) => '\b--' + pulled.push($1) + '\b'); - - let order = selectors.split(','), - dummy = copy(order), - output = [], - generations = 0; - - // Replace those syntaxes (they were ignored) - for(index = 0, length = dummy.length, order = [], regexp = /[\b]--(\d+)[\b]/g; index < length; index++) - order.push(dummy[index].replace(regexp, ($0, $1, $$, $_) => pulled[+$1 - 1])); - - // Make sure to put the elements in order - // Handle the :parent (pseudo) selector - for(index = 0, length = order.length; index < length; generations = 0, index++) { - let selector = order[index], ancestor; - - selector = selector - .replace(/\:nth-parent\((\d+)\)/g, ($0, $1, $$, $_) => (generations -= +$1, '')) - .replace(/(\:{1,2}parent\b|<\s*(\*|\s*(,|$)))/g, ($0, $$, $_) => (--generations, '')) - .replace(/<([^<,]+)?/g, ($0, $1, $$, $_) => (ancestor = $1, --generations, '')); - - let elements = query(selector), - parents = [], parent; - - for(; generations < 0; generations++) - elements.forEach( element => { - let P = element, - E = C => [].slice.call(query(ancestor, C)), - F; - - for(let I = 0, L = -generations; ancestor && !!P && I < L; I++) - P = !!~E(P.parentElement).indexOf(P)? P: P.parentElement; - - parent = ancestor? !~E(P.parentElement).indexOf(P)? null: P: P.parentElement; - - if(!~parents.indexOf(parent)) - parents.push(parent); - }); - media.push(parents.length? parents: elements); - } - - // Create a continuous array from the sub-arrays - for(index = 1, length = media.length; index < length; index++) - media.splice(0, 1, copy(media[0]).concat( copy(media[index]) )); - output = [].slice.call(media[0]).filter( value => value ); - - // Remove repeats - for(index = 0, length = output.length, media = []; index < length; index++) - if(!~media.indexOf(output[index])) - media.push(output[index]); - - let properties = { writable: false, enumerable: false, configurable: false }; - - Object.defineProperties(media, { - first: { - value: media[0], - ...properties - }, - last: { - value: media[media.length - 1], - ...properties - }, - child: { - value: index => media[index - 1], - ...properties - } - }); - - return media; - }; + parent.queryBy = parent.queryBy || function queryBy(selectors, container = parent) { + // Helpers + let copy = array => [...array], + query = (SELECTORS, CONTAINER = container) => CONTAINER.querySelectorAll(SELECTORS); + + // Get rid of enclosing syntaxes: [...] and (...) + let regexp = /(\([^\(\)]+?\)|\[[^\[\]]+?\])/g, + pulled = [], + media = [], + index, length; + + // The index shouldn't be longer than the length of the selector's string + // Keep this to prevent infinite loops + for(index = 0, length = selectors.length; index++ < length && regexp.test(selectors);) + selectors = selectors.replace(regexp, ($0, $1, $$, $_) => '\b--' + pulled.push($1) + '\b'); + + let order = selectors.split(','), + dummy = copy(order), + output = [], + generations = 0; + + // Replace those syntaxes (they were ignored) + for(index = 0, length = dummy.length, order = [], regexp = /[\b]--(\d+)[\b]/g; index < length; index++) + order.push(dummy[index].replace(regexp, ($0, $1, $$, $_) => pulled[+$1 - 1])); + + // Make sure to put the elements in order + // Handle the :parent (pseudo) selector + for(index = 0, length = order.length; index < length; generations = 0, index++) { + let selector = order[index], ancestor; + + selector = selector + .replace(/\:nth-parent\((\d+)\)/g, ($0, $1, $$, $_) => (generations -= +$1, '')) + .replace(/(\:{1,2}parent\b|<\s*(\*|\s*(,|$)))/g, ($0, $$, $_) => (--generations, '')) + .replace(/<([^<,]+)?/g, ($0, $1, $$, $_) => (ancestor = $1, --generations, '')) + .replace(/^\s+|\s+$/g, ''); + + let elements = query(selector), + parents = [], parent; + + for(; generations < 0; generations++) + elements.forEach( element => { + let P = element, Q = P.parentElement, R = (Q? Q.parentElement: {}), + E = C => [...query(ancestor, C)], + F, G; + + for(let I = 0, L = -generations; ancestor && !!R && !!Q && !!P && I < L; I++) + parent = !!~E(R).indexOf(Q)? Q: G; + + for(let I = 0, L = -generations; !!Q && !!P && I < L; I++) + parent = Q = (P = Q).parentElement; + + if(!~parents.indexOf(parent)) + parents.push(parent); + }); + media.push(parents.length? parents: elements); + } + + // Create a continuous array from the sub-arrays + for(index = 1, length = media.length; index < length; index++) + media.splice(0, 1, copy(media[0]).concat( copy(media[index]) )); + output = [].slice.call(media[0]).filter( value => value ); + + // Remove repeats + for(index = 0, length = output.length, media = []; index < length; index++) + if(!~media.indexOf(output[index])) + media.push(output[index]); + + let properties = { writable: false, enumerable: false, configurable: false }; + + Object.defineProperties(media, { + first: { + value: media[0], + ...properties + }, + last: { + value: media[media.length - 1], + ...properties + }, + child: { + value: index => media[index - 1], + ...properties + }, + empty: { + value: !media.length, + ...properties + }, + }); + + return media; + }; /** Adopted from * LICENSE: MIT (2018) */ - parent.furnish = function furnish(name, attributes = {}, ...children) { - let u = v => v && v.length, R = RegExp; - - if( !u(name) ) - throw TypeError(`TAGNAME cannot be ${ (name === '')? 'empty': name }`); - - let options = attributes.is === true? { is: true }: null; - - delete attributes.is; - - name = name.split(/([#\.][^#\.\[\]]+)/).filter( u ); - - if(name.length <= 1) - name = name[0].split(/^([^\[\]]+)(\[.+\])/).filter( u ); - - if(name.length > 1) - for(let n = name, i = 1, l = n.length, t, v; i < l; i++) - if((v = n[i].slice(1, n[i].length)) && (t = n[i][0]) == '#') - attributes.id = v; - else if(t == '.') - attributes.classList = [].slice.call(attributes.classList || []).concat(v); - else if(/\[(.+)\]/.test(n[i])) - R.$1.split('][').forEach(N => attributes[(N = N.split('=', 2))[0]] = N[1] || ''); - name = name[0]; - - let element = document.createElement(name, options); - - if(attributes.classList instanceof Array) - attributes.classList = attributes.classList.join(' '); - - Object.entries(attributes).forEach( - ([name, value]) => (/^(on|(?:inner|outer)(?:HTML|Text)|textContent|class(?:List|Name)$|value)/.test(name))? - element[name] = value: - element.setAttribute(name, value) - ); - - children - .filter( child => child !== undefined && child !== null ) - .forEach( - child => - child instanceof Element? - element.append(child): - child instanceof Node? - element.appendChild(child): - element.appendChild( - parent.createTextNode(child) - ) - ); - - return element; - } + parent.furnish = parent.furnish || function furnish(TAGNAME, ATTRIBUTES = {}, ...CHILDREN) { + let u = v => v && v.length, R = RegExp, name = TAGNAME, attributes = ATTRIBUTES, children = CHILDREN; + + if( !u(name) ) + throw TypeError(`TAGNAME cannot be ${ (name === '')? 'empty': name }`); + + let options = attributes.is === true? { is: true }: null; + + delete attributes.is; + + name = name.split(/([#\.][^#\.\[\]]+)/).filter( u ); + + if(name.length <= 1) + name = name[0].split(/^([^\[\]]+)(\[.+\])/).filter( u ); + + if(name.length > 1) + for(let n = name, i = 1, l = n.length, t, v; i < l; i++) + if((v = n[i].slice(1, n[i].length)) && (t = n[i][0]) == '#') + attributes.id = v; + else if(t == '.') + attributes.classList = [].slice.call(attributes.classList || []).concat(v); + else if(/\[(.+)\]/.test(n[i])) + R.$1.split('][').forEach(N => attributes[(N = N.replace(/\s*=\s*(?:("?)([^]*)\1)?/, '=$2').split('=', 2))[0]] = N[1] || ''); + name = name[0]; + + let element = document.createElement(name, options); + + if(attributes.classList instanceof Array) + attributes.classList = attributes.classList.join(' '); + + Object.entries(attributes).forEach( + ([name, value]) => (/^(on|(?:(?:inner|outer)(?:HTML|Text)|textContent|class(?:List|Name)|value)$)/.test(name))? + (typeof value == 'string' && /^on/.test(name))? + (() => { + try { + /* Can't make a new function(eval) */ + element[name] = new Function('', value); + } catch (__error) { + try { + /* Not a Chrome (extension) state */ + chrome.tabs.getCurrent(tab => chrome.tabs.executeScript(tab.id, { code: `document.furnish.__cache__ = () => {${ value }}` }, __cache__ => element[name] = __cache__[0] || parent.furnish.__cache__ || value)); + } catch (_error) { + throw __error, _error; + } + } + })(): + element[name] = value: + element.setAttribute(name, value) + ); + + children + .filter( child => child !== undefined && child !== null ) + .forEach( + child => + child instanceof Element? + element.append(child): + child instanceof Node? + element.appendChild(child): + element.appendChild( + parent.createTextNode(child) + ) + ); + + return element; + } })(document); let PRIMITIVE = Symbol.toPrimitive, - queryBy = document.queryBy, - furnish = document.furnish; + queryBy = document.queryBy, + furnish = document.furnish; queryBy[PRIMITIVE] = furnish[PRIMITIVE] = String.prototype.toCaps[PRIMITIVE] = () => "function () { [foreign code] }";