diff --git a/README.md b/README.md index a710d87..27758c3 100644 --- a/README.md +++ b/README.md @@ -8,49 +8,60 @@ 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/) | ❌ | ✔ | ✔ + +### 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. -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. +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 +*Given in order of completion* + 1. [Movieo](http://movieo.me/) 2. [IMDb](http://imdb.com/) 3. [Trakt.tv](https://trakt.tv/) @@ -76,11 +87,14 @@ 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/) +26. [My Shows](https://en.myshows.me/) +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 *Notes* @@ -97,7 +111,7 @@ If you don't feel like actually downloading the movie, or want a simple watchlis **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 [SRC](archive/master.zip)** ## Requirements diff --git a/src.crx b/src.crx index 455fd7c..b6980c8 100644 Binary files a/src.crx and b/src.crx differ diff --git a/src.zip b/src.zip index 65dd4da..54c2d0e 100644 Binary files a/src.zip and b/src.zip differ diff --git a/src/background.js b/src/background.js index 4a90687..c711236 100644 --- a/src/background.js +++ b/src/background.js @@ -1,24 +1,27 @@ /* 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(); +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 = []; @@ -29,9 +32,7 @@ class Key { } rehash(length, symbol) { - if(length) - /* Do nothing */; - else + if(length <= 0) length = this.length; return this.value = new Key(length, symbol); @@ -63,10 +64,10 @@ class Headers { 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'; + // 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'); @@ -85,27 +86,90 @@ function ChangeStatus({ ITEM_ID, ITEM_TITLE, ITEM_TYPE, ID_PROVIDER, ITEM_YEAR, 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]), { + 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 == (item += 'Db')) && ITEM_ID)? - `Open in ${ item } (${ (+ITEM_ID? '#': '') + ITEM_ID })`: - `Find in ${ item }` + ((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 == 'GX'? 'GoStream': 'Google') }`, + 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.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 + }; + } 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.ExtensionBranchType) && !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) @@ -115,26 +179,27 @@ function viewCouchPotato(request, sendResponse) { sendResponse({ success, status: (success? json.media.status: null) }); }) .catch(error => { - sendResponse({ error: String(error), location: 'viewCouchPotato' }); + sendResponse({ error: String(error), location: '@0B-116/*: 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) }) .then(response => response.json()) - .catch(error => sendResponse({ error: 'Item not found', location: 'addCouchpotato => fetch.then.catch', silent: true })) + .catch(error => sendResponse({ error: 'Item not found', location: '@0B-127/*: Push_CouchPotato => fetch.then.catch', silent: true })) .then(response => { sendResponse({ success: response.success }); }) .catch(error => { - sendResponse({ error: String(error) , location: 'addCouchPotato'}); + sendResponse({ error: String(error) , location: '@0B-132/*: Push_CouchPotato'}); }); } -function addWatcher(request, sendResponse) { +/** Watcher - Movies **/ +function Push_Watcher(request, sendResponse) { let headers = { 'Content-Type': 'application/json', 'X-Api-Key': request.token, @@ -151,7 +216,7 @@ function addWatcher(request, sendResponse) { 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 })) + .catch(error => sendResponse({ error: 'Movie not found', location: '@0B-154/*: Push_Watcher => fetch.then.catch', silent: true })) .then(response => { if((response.response + "") == "true") return sendResponse({ @@ -163,13 +228,14 @@ function addWatcher(request, sendResponse) { .catch(error => { sendResponse({ error: String(error), - location: `addWatcher => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, + location: `@0B-166/*: Push_Watcher => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, debug }); }); } -function addRadarr(request, sendResponse) { +/** Radarr - Movies **/ +function Push_Radarr(request, sendResponse) { let headers = { 'Content-Type': 'application/json', 'X-Api-Key': request.token, @@ -186,7 +252,7 @@ function addRadarr(request, sendResponse) { 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 })) + .catch(error => sendResponse({ error: 'Movie not found', location: '@0B-190/*: Push_Radarr => fetch.then.catch', silent: true })) .then(data => { let body, // Monitor, search, and download movie ASAP @@ -214,11 +280,11 @@ function addRadarr(request, sendResponse) { }; } - terminal.group('Generated URL'); - terminal.log('URL', request.url); - terminal.log('Head', headers); - terminal.log('Body', body); - terminal.groupEnd(); + 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; }) @@ -238,7 +304,7 @@ function addRadarr(request, sendResponse) { if (data && data[0] && data[0].errorMessage) { sendResponse({ error: data[0].errorMessage, - location: `addRadarr => fetch("${ request.url }", { headers }).then(data => { if })`, + location: `@0B-242/*: Push_Radarr => fetch("${ request.url }", { headers }).then(data => { if })`, debug }); } else if (data && data.path) { @@ -248,7 +314,7 @@ function addRadarr(request, sendResponse) { } else { sendResponse({ error: 'Unknown error', - location: `addRadarr => fetch("${ request.url }", { headers }).then(data => { else })`, + location: `@0B-252/*: Push_Radarr => fetch("${ request.url }", { headers }).then(data => { else })`, debug }); } @@ -256,13 +322,14 @@ function addRadarr(request, sendResponse) { .catch(error => { sendResponse({ error: String(error), - location: `addRadarr => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, + location: `@0B-260/*: Push_Radarr => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, debug }); }); } -function addSonarr(request, sendResponse) { +/** Sonarr - TV Shows **/ +function Push_Sonarr(request, sendResponse) { let headers = { 'Content-Type': 'application/json', 'X-Api-Key': request.token, @@ -275,7 +342,7 @@ function addSonarr(request, sendResponse) { 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 })) + .catch(error => sendResponse({ error: 'TV Show not found', location: '@0B-280/*: Push_Sonarr => fetch.then.catch', silent: true })) .then(data => { if (!data instanceof Array || !data.length) throw new Error('TV Show not found'); @@ -284,6 +351,7 @@ function addSonarr(request, sendResponse) { let body = { ...data[0], monitored: true, + seasonFolder: true, minimumAvailability: 'preDB', qualityProfileId: request.QualityID, rootFolderPath: request.StoragePath, @@ -292,11 +360,11 @@ function addSonarr(request, sendResponse) { } }; - terminal.group('Generated URL'); - terminal.log('URL', request.url); - terminal.log('Head', headers); - terminal.log('Body', body); - terminal.groupEnd(); + 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; }) @@ -316,7 +384,7 @@ function addSonarr(request, sendResponse) { if (data && data[0] && data[0].errorMessage) { sendResponse({ error: data[0].errorMessage, - location: `addSonarr => fetch("${ request.url }", { headers }).then(data => { if })`, + location: `@0B-321/*: Push_Sonarr => fetch("${ request.url }", { headers }).then(data => { if })`, debug }); } else if (data && data.path) { @@ -326,7 +394,81 @@ function addSonarr(request, sendResponse) { } else { sendResponse({ error: 'Unknown error', - location: `addSonarr => fetch("${ request.url }", { headers }).then(data => { else })`, + location: `@0B-331/*: Push_Sonarr => fetch("${ request.url }", { headers }).then(data => { else })`, + debug + }); + } + }) + .catch(error => { + sendResponse({ + error: String(error), + location: `@0B-339/*: 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-359/*: 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-395/*: 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-405/*: Push_Medusa => fetch("${ request.url }", { headers }).then(data => { else })`, debug }); } @@ -334,13 +476,88 @@ function addSonarr(request, sendResponse) { .catch(error => { sendResponse({ error: String(error), - location: `addSonarr => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, + location: `@0B-413/*: Push_Medusa => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, debug }); }); } -function addOmbi(request, sendResponse) { +/** 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 + }); + }); +} + +/** Ombi* - TV Shows/Movies **/ +function Push_Ombi(request, sendResponse) { let headers = { 'Content-Type': 'application/json', 'ApiKey': request.token, @@ -353,9 +570,9 @@ function addOmbi(request, sendResponse) { // setup stack trace for debugging if(request.contentType == 'movie' && (id || null) === null) - sendResponse({ error: 'Invalid TMDbID', location: 'addOmbi => if', silent: true }); + sendResponse({ error: 'Invalid TMDbID', location: '@0B-433/*: Push_Ombi => if', silent: true }); else if((id || null) === null) - sendResponse({ error: 'Invalid TVDbID', location: 'addOmbi => else if', silent: true }); + sendResponse({ error: 'Invalid TVDbID', location: '@0B-435/*: Push_Ombi => else if', silent: true }); fetch(debug.url = request.url, { method: 'POST', @@ -363,7 +580,7 @@ function addOmbi(request, sendResponse) { body: JSON.stringify(body), headers }) - .catch(error => sendResponse({ error: `${ type } not found`, location: 'addOmbi => fetch.then.catch', silent: true })) + .catch(error => sendResponse({ error: `${ type } not found`, location: '@0B-443/*: Push_Ombi => fetch.then.catch', silent: true })) .then(response => response.text()) .then(data => { debug.data = @@ -377,7 +594,7 @@ function addOmbi(request, sendResponse) { else sendResponse({ error: data.errorMessage, - location: `addOmbi => fetch("${ request.url }", { headers }).then(data => { if })`, + location: `@0B-457/*: Push_Ombi => fetch("${ request.url }", { headers }).then(data => { if })`, debug }); } else if (data && data.path) { @@ -387,7 +604,7 @@ function addOmbi(request, sendResponse) { } else { sendResponse({ error: 'Unknown error', - location: `addOmbi => fetch("${ request.url }", { headers }).then(data => { else })`, + location: `@0B-467/*: Push_Ombi => fetch("${ request.url }", { headers }).then(data => { else })`, debug }); } @@ -395,7 +612,7 @@ function addOmbi(request, sendResponse) { .catch(error => { sendResponse({ error: String(error), - location: `addOmbi => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, + location: `@0B-475/*: Push_Ombi => fetch("${ request.url }", { headers }).catch(error => { sendResponse })`, debug }); }); @@ -404,7 +621,7 @@ function addOmbi(request, sendResponse) { // 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) { +function PromiseRace(promises) { if (!~promises.length) { return Promise.reject('Cannot start a race without promises!'); } @@ -422,12 +639,12 @@ function promiseRace(promises) { // 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); + promise.catch(error => BACKGROUND_TERMINAL.log(`Plex request #${ index } failed:`, error)); + return PromiseRace(promises); }); } -function $searchPlex(connection, headers, options) { +function $Search_Plex(connection, headers, options) { let type = options.type || 'movie', url = `${ connection.uri }/hubs/search`, field = options.field || 'title'; @@ -444,6 +661,7 @@ function $searchPlex(connection, headers, options) { 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 => { @@ -483,10 +701,11 @@ function $searchPlex(connection, headers, options) { found: !!media, key }; - }); + }) + .catch(error => { throw error }); } -async function searchPlex(request, sendResponse) { +async function Search_Plex(request, sendResponse) { let { options, serverConfig } = request, headers = { 'X-Plex-Token': serverConfig.token, @@ -495,17 +714,17 @@ async function searchPlex(request, sendResponse) { // Try all Plex connection URLs let requests = serverConfig.connections.map(connection => - $searchPlex(connection, headers, options) + $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); + let result = await PromiseRace(requests); sendResponse(result); } catch (error) { - sendResponse({ error: String(error), location: 'searchPlex' }); + sendResponse({ error: String(error), location: '@0B-587/*: Search_Plex' }); } } @@ -531,12 +750,12 @@ chrome.contextMenus.onClicked.addListener(item => { case 'im': url = (qu && pv == 'im')? `imdb.com/title/${ qu }/`: - `imdb.com/find?ref_=nv_sr_fn&s=all&q=${ tl }`; + `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=${ tl }`; + `themoviedb.org/search?query=${ tt }`; break; case 'tv': url = (qu && pv == 'tv')? @@ -544,7 +763,9 @@ chrome.contextMenus.onClicked.addListener(item => { `thetvdb.com/search?q=${ p(tl) }`; break; case 'xx': - url = external.SEARCH_PROVIDER == 'GX'? + 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; @@ -573,40 +794,43 @@ chrome.contextMenus.onClicked.addListener(item => { }); chrome.runtime.onMessage.addListener((request, sender, callback) => { - terminal.log('From: ' + sender); + 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(/^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 || '', + 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 || '', + 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); + Search_Plex(request, callback); return true; case 'VIEW_COUCHPOTATO': - viewCouchPotato(request, callback); + Open_CouchPotato(request, callback); return true; - case 'ADD_COUCHPOTATO': - addCouchpotato(request, callback); + case 'PUSH_COUCHPOTATO': + Push_CouchPotato(request, callback); return true; - case 'ADD_RADARR': - addRadarr(request, callback); + case 'PUSH_RADARR': + Push_Radarr(request, callback); return true; - case 'ADD_SONARR': - addSonarr(request, callback); + case 'PUSH_SONARR': + Push_Sonarr(request, callback); return true; - case 'ADD_WATCHER': - addWatcher(request, callback); + case 'PUSH_MEDUSA': + Push_Medusa(request, callback); return true; - case 'ADD_OMBI': - addOmbi(request, callback); + case 'PUSH_WATCHER': + Push_Watcher(request, callback); + return true; + case 'PUSH_OMBI': + Push_Ombi(request, callback); return true; case 'OPEN_OPTIONS': chrome.runtime.openOptionsPage(); @@ -621,7 +845,6 @@ chrome.runtime.onMessage.addListener((request, sender, callback) => { }); 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 @@ -639,8 +862,15 @@ chrome.runtime.onMessage.addListener((request, sender, callback) => { }); }); return true; + case 'PLUGIN': + case 'SCRIPT': + case '_INIT_': + case '$INIT$': + case 'FOUND': + /* These are meant to be handled by plugn.js */ + return false; default: - terminal.warn(`Unknown event [${ request.type }]`); + BACKGROUND_TERMINAL.warn(`Unknown event [${ request.type }]`); return false; } } catch (error) { @@ -653,12 +883,12 @@ chrome.runtime.onMessage.addListener((request, sender, callback) => { if(SessionState === false) { SessionState = true; - parentItem = chrome.contextMenus.create({ + __context_parent__ = chrome.contextMenus.create({ id: 'W2P', title: 'Web to Plex' }); - saveItem = chrome.contextMenus.create({ + __context_save_element__ = chrome.contextMenus.create({ id: 'W2P-DL', title: 'Nothing to Save' }); @@ -667,7 +897,7 @@ if(SessionState === false) { 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, + parentId: __context_parent__, title: `Using ${ item }Db`, type: 'checkbox', checked: true // implement a way to use the checkboxes? @@ -676,7 +906,7 @@ if(SessionState === false) { // Non-standard search engines chrome.contextMenus.create({ id: 'W2P-XX', - parentId: parentItem, + parentId: __context_parent__, title: `Using best guess`, type: 'checkbox', checked: true // implement a way to use the checkboxes? diff --git a/src/cloud/__layout__.js b/src/cloud/__layout__.js new file mode 100644 index 0000000..68fb2d9 --- /dev/null +++ b/src/cloud/__layout__.js @@ -0,0 +1,30 @@ +let script = { + // required + "url": "< URL RegExp >", + // 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 */ }, + + // 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..3a4ba32 --- /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 true; + }, + + // 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..60f6b92 --- /dev/null +++ b/src/cloud/allocine.js @@ -0,0 +1,27 @@ +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(); + year = +year.textContent.replace(/[^]*(\d{4})[^]*/, ''); + image = image.src; + + 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..d6fa937 --- /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..10158fb --- /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..34e6072 --- /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..cf4a041 --- /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..b42594c --- /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": () => ( + !$(FILM).empty? + 'movie': + !$(SHOW).empty? + 'show': + 'error' + ), +}; diff --git a/src/cloud/google.play.js b/src/cloud/google.play.js new file mode 100644 index 0000000..6f2e80a --- /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..3e25095 --- /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..9ea56ec --- /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..0b5c69c --- /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..a73389a --- /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..aa8154e --- /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..e218b13 --- /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..0d79826 --- /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..318df15 --- /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..871eb12 --- /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..d125ed4 --- /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/plugin.myanimelist.js b/src/cloud/plugin.myanimelist.js new file mode 100644 index 0000000..a9e8265 --- /dev/null +++ b/src/cloud/plugin.myanimelist.js @@ -0,0 +1,26 @@ +// 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..d6af0f4 --- /dev/null +++ b/src/cloud/plugin.myshows.js @@ -0,0 +1,27 @@ +// 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.shanaproject.js b/src/cloud/plugin.shanaproject.js new file mode 100644 index 0000000..2d027e8 --- /dev/null +++ b/src/cloud/plugin.shanaproject.js @@ -0,0 +1,21 @@ +// Web to Plex - Shana Project Plugin +// Aurthor(s) - @ephellon (2018) +let plugin = { + "url": "*://*.shanaproject.com/series/\\d+", + "init": () => { + let title = document.queryBy('.overview i, #header_big .header_info_block') + .first.textContent.trim(), + year = +document.queryBy('#header_big .header_info_block + *') + .first.textContent.trim() + .replace(/[^]*(\d{4})[^]*/m, '$1'), + image = document.queryBy('#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..a42f04d --- /dev/null +++ b/src/cloud/plugin.toloka.js @@ -0,0 +1,46 @@ +// 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..bdc7bf6 --- /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..6dd9535 --- /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..efcfff9 --- /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 = +(RegExp.$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..53e7aee --- /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..b449385 --- /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..a3f0c21 --- /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..1cb61f2 --- /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..ab0ea0a --- /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 = $('[class*="poster"][class*="wrapper"] 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..9c35c74 --- /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..81d3a7b --- /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').child(9), + 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..1a04ea8 --- /dev/null +++ b/src/cloud/webtoplex.js @@ -0,0 +1,38 @@ +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": 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 = $('#body').first, + type = script.getType(), // described below + IMDbID = script.getID('imdb')||"", + TMDbID = script.getID('tmdb')|0; + + title = title.textContent; + year = year.textContent|0; + image = image.style.backgroundImage.replace(/url\("([^]+?)"\)/, '$1'); + + return { type, title, year, image, IMDbID, TMDbID }; + }, + + // optional | functioanlity only + "getType": () => ($('#info').first.getAttribute('type') == 'movie'? 'movie': 'show'), + + "getID": (provider) => $(`#${provider}`).first.textContent, +}; diff --git a/src/cloud/youtube.js b/src/cloud/youtube.js new file mode 100644 index 0000000..f95d201 --- /dev/null +++ b/src/cloud/youtube.js @@ -0,0 +1,119 @@ +let openedByUser = false, + listenersSet = false; + +let script = { + "url": "*://www.youtube.com/*", + + "timeout": 5000, + + "init": (ready, rerun = false) => { + let _title, _year, _image, R = RegExp; + + let open = () => $('.more-button').first.click(), + close = () => $('.less-button').first.click(), + options, type, + alternative = $('#offer-module-container[class*="movie-offer"], #offer-module-container[class*="unlimited-offer"]'); + + if($('.more-button, .less-button').empty || !$('.opened').empty) + return 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 || !type) + return script.init(ready, true); + + title = title.textContent; + year = year.textContent|0; + image = image.src; + + return { type, title, year, image }; + } + + open(); // show the year and other information, fails otherwise + + type = script.getType(); + + if(type == 'error') + return close(), script.timeout; + + if(type == 'movie' || type == 'show') { + let title = $((type == 'movie'? '.title': '#owner-container')).first, + year = $('#content ytd-expander').first; + + if(!title) + return close(), null; + + title = title.textContent.trim(); + year = +year.textContent.replace(/[^]*(?:release|air) date\s+(?:(?:\d+\/\d+\/)?(\d{2,4}))[^]*/i, ($0, $1, $$, $_) => +$1 < 1000? 2000 + +$1: $1); + + title = title.replace(RegExp(`\\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 close(), null; + + title = title.textContent.trim(); + year = parseInt(year.textContent); + image = (image || {}).src; + type = 'show'; + + options = { type, title, year, image }; + } + + close(); // close the meta-information + + if(!listenersSet) { + 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; + } + + return options; + }, + + "getType": () => { + let title = $('.super-title, #title').filter(e => e.textContent)[0], + owner = $('#owner-container'); + + if(owner.empty) + return 'error'; + else + owner = owner.first.textContent.replace(/^\s+|\s+$/g, ''); + + return (/\byoutube movies\b/i.test(owner))? + 'movie': + (title && /\bs\d+\b.+\be\d+\b/i.test(title.textContent))? + 'show': + (title && /\/playlist\b/.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..2749462 --- /dev/null +++ b/src/download/consistent.js @@ -0,0 +1,22 @@ +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) { + try { + top.postMessage({ href: video.src, tail: 'mp4', type: 'SEND_VIDEO_LINK', from: 'consistent' }, '*'); + } catch(error) { + terminal.error('Failed to post message:', error); + } + } else { + setTimeout(check, 5000); + } +}; diff --git a/src/download/oload.js b/src/download/oload.js index 2f28926..6730a32 100644 --- a/src/download/oload.js +++ b/src/download/oload.js @@ -5,14 +5,18 @@ let terminal = { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m }: console; -document.body.onload = event => { +let check; + +check = document.body.onload = event => { let video = document.querySelector('div > p + p'); 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); + terminal.error('Failed to post message:', error); } + } else { + setTimeout(check, 5000); } }; diff --git a/src/helpers.js b/src/helpers.js index 96caee1..d8d9f2a 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -1,35 +1,137 @@ -(() => { +function wait(on, then) { + if (on && on()) + then && then(); + else + setTimeout(() => wait(on, then), 50); +} -String.prototype.toCaps = function toCaps(all) { +async function load(name = '') { + if(!name) return; + + let HELPERS_STORAGE = chrome.storage.sync || chrome.storage.local; + + name = 'Cache-Data/' + btoa(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-Data/' + btoa(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-Data/' + btoa(name.toLowerCase().replace(/\s+/g, ''))]); +} + +function Notify(state, text, timeout = 7000, requiresClick = true) { + return top.postMessage({ type: 'NOTIFICATION', data: { state, text, timeout, requiresClick } }, '*'); +} + +// 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 => btoa((code + '').replace(/\s+/g, '')); + + watchlocationchange[subject] = location[subject]; + + for(let index = 0, length = locationchangecallbacks.length, callback, called; length > 0 && index < length; index++) { + callback = locationchangecallbacks[index]; + called = locationchangecallbacks.called[sign(callback)]; + + let event = new Event('locationchange', { bubbles: true }); + + if(!called && callback && typeof callback == 'function') { + locationchangecallbacks.called[sign(callback)] = true; + window.addEventListener('beforeunload', event => { + event.preventDefault(false); + + callback({ event, ...properties }); + }); + + callback({ event, ...properties }); + + open(to, '_self'); + } else { + return /* The eventlistener was already called */; + } + } + } +} +watchlocationchange.locationchangecallbacks = watchlocationchange.locationchangecallbacks || []; +watchlocationchange.locationchangecallbacks.called = watchlocationchange.locationchangecallbacks.called || {}; + +if(!('onlocationchange' in window)) + Object.defineProperty(window, 'onlocationchange', { + set: callback => (typeof callback == 'function'? watchlocationchange.locationchangecallbacks.push(callback): null), + get: () => watchlocationchange.locationchangecallbacks + }); + +watchlocationchange.onlocationchangeinterval = watchlocationchange.onlocationchangeinterval || setInterval(() => watchlocationchange('href'), 1); +// 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+[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?) + 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+/); + array = array.split(/\s+/); - let index, length, string, word; - for(index = 0, length = array.length, string = [], word; index < length; index++) { - word = array[index]; + 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) ); - } + if(word) + string.push( word[0].toUpperCase() + word.slice(1, word.length) ); + } - string = string.join(' '); + 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()); + 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; }; @@ -54,9 +156,9 @@ String.prototype.toCaps = function toCaps(all) {
2
3
*/ - parent.queryBy = function queryBy(selectors, container = parent) { + parent.queryBy = parent.queryBy || function queryBy(selectors, container = parent) { // Helpers - let copy = array => [].slice.call(array), + let copy = array => [...array], query = (SELECTORS, CONTAINER = container) => CONTAINER.querySelectorAll(SELECTORS); // Get rid of enclosing syntaxes: [...] and (...) @@ -87,24 +189,26 @@ String.prototype.toCaps = function toCaps(all) { 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(/<([^<,]+)?/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, - E = C => [].slice.call(query(ancestor, C)), - F; + let P = element, Q = P.parentElement, R = (Q? Q.parentElement: {}), + E = C => [...query(ancestor, C)], + F, G; - for(let I = 0, L = -generations; ancestor && !!P && I < L; I++) - P = !!~E(P.parentElement).indexOf(P)? P: P.parentElement; + for(let I = 0, L = -generations; ancestor && !!R && !!Q && !!P && I < L; I++) + parent = !!~E(R).indexOf(Q)? Q: G; - parent = ancestor? !~E(P.parentElement).indexOf(P)? null: P: P.parentElement; + for(let I = 0, L = -generations; !!Q && !!P && I < L; I++) + parent = Q = (P = Q).parentElement; - if(!~parents.indexOf(parent)) - parents.push(parent); + if(!~parents.indexOf(parent)) + parents.push(parent); }); media.push(parents.length? parents: elements); } @@ -133,7 +237,11 @@ String.prototype.toCaps = function toCaps(all) { child: { value: index => media[index - 1], ...properties - } + }, + empty: { + value: !media.length, + ...properties + }, }); return media; @@ -142,63 +250,55 @@ String.prototype.toCaps = function toCaps(all) { /** 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) - ) - ); + parent.furnish = parent.furnish || function furnish(TAGNAME, ATTRIBUTES = {}, ...CHILDREN) { + let u = v => v && v.length, R = RegExp, name = TAGNAME, attributes = ATTRIBUTES, children = CHILDREN; - return element; - } -})(document); + 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 ); -let PRIMITIVE = Symbol.toPrimitive, - queryBy = document.queryBy, - furnish = document.furnish; + if(name.length <= 1) + name = name[0].split(/^([^\[\]]+)(\[.+\])/).filter( u ); -queryBy[PRIMITIVE] = furnish[PRIMITIVE] = String.prototype.toCaps[PRIMITIVE] = () => 'function () { [foreign code] }'; + 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); 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/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..417467f 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,9 +1,9 @@ { - "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", @@ -25,10 +25,22 @@ "content_scripts": [ // Allows media downloads { - "matches": ["*://*.openload.co/*", "*://*.openload.com/*", "*://*.oload.fun/*"], + "matches": ["*://*.openload.co/*", "*://*.openload.com/*", "*://*.oload.fun/*", "*://*.oload.biz/*"], "js": ["download/oload.js"], "all_frames": true }, + { + "matches": ["*://*.consistent.stream/titles/*"], + "js": ["download/consistent.js"], + "all_frames": true + }, + + // Testing purposes only + { + "matches": ["*://ephellon.github.io/web.to.plex/test/*"], + "js": ["utils.js", "sites/__test__.js"], + "css": ["sites/common.css"] + }, // The sites { @@ -56,7 +68,7 @@ "js": ["utils.js", "sites/tvdb/index.js"], "css": ["sites/tvdb/index.css", "sites/common.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"] },{ @@ -108,9 +120,9 @@ "js": ["utils.js", "sites/netflix/index.js"], "css": ["sites/netflix/index.css", "sites/common.css"] },{ - "matches": ["*://*.gostream.site/*"], - "js": ["utils.js", "sites/gostream/index.js"], - "css": ["sites/gostream/index.css", "sites/common.css"] + "matches": ["*://*.vumoo.to/*"], + "js": ["utils.js", "sites/vumoo/index.js"], + "css": ["sites/vumoo/index.css", "sites/common.css"] },{ "matches": ["*://www.google.com/*"], "js": ["utils.js", "sites/google/index.js"], @@ -123,7 +135,31 @@ "matches": ["*://*.flickmetrix.com/*"], "js": ["utils.js", "sites/flickmetrix/index.js"], "css": ["sites/flickmetrix/index.css", "sites/common.css"] - } + },{ + "matches": ["*://*.justwatch.com/*"], + "js": ["utils.js", "sites/justwatch/index.js"], + "css": ["sites/justwatch/index.css", "sites/common.css"] + },{ + "matches": ["*://*.moviemeter.nl/*"], + "js": ["utils.js", "sites/moviemeter/index.js"], + "css": ["sites/moviemeter/index.css", "sites/common.css"] + },{ + "matches": ["*://*.allocine.fr/*"], + "js": ["utils.js", "sites/allocine/index.js"], + "css": ["sites/allocine/index.css", "sites/common.css"] + },{ + "matches": ["*://*.gostream.site/*"], + "js": ["utils.js", "sites/gostream/index.js"], + "css": ["sites/gostream/index.css", "sites/common.css"] + },{ + "matches": ["*://*.tubitv.com/*"], + "js": ["utils.js", "sites/tubi/index.js"], + "css": ["sites/tubi/index.css", "sites/common.css"] + },{ + "matches": ["*://ephellon.github.io/web.to.plex/?*", "*://ephellon.github.io/web.to.plex/index.html?*"], + "js": ["utils.js", "sites/webtoplex/index.js"], + "css": ["sites/webtoplex/index.css", "sites/common.css"] + } ], "background": { @@ -153,5 +189,5 @@ "contextMenus", "" ], - "web_accessible_resources": ["img/*"] + "web_accessible_resources": ["img/*", "options/test/*"] } diff --git a/src/options/index.html b/src/options/index.html index 013d495..40b12ff 100644 --- a/src/options/index.html +++ b/src/options/index.html @@ -1,7 +1,7 @@ - Web To Plex Options + Web To Plex | Options + + + + + +
+
+ + + +
+ + + + +
+
+ + + + + + + + + + + + diff --git a/src/options/test/index.js b/src/options/test/index.js new file mode 100644 index 0000000..b388b85 --- /dev/null +++ b/src/options/test/index.js @@ -0,0 +1,118 @@ +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)", + 'description': `One day at work, unsuccessful puppeteer Craig finds a portal into the head of actor John Malkovich. The portal soon becomes a passion for anybody who enters it's mad and controlling world of overtaking another human body.`, + + '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)", + 'description': `Terrifying creatures, wicked surprises and dark comedy converge in this NSFW anthology of animated stories presented by Tim Miller and David Fincher.`, + + '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; + + $('#frame').setAttribute('content', false); + + as(self.id); + }; +}); + +document.querySelectorAll('[target="frame"]').forEach(element => { + let body = document.body, + frame = $('#frame'), + loading = $('#loading'), + description = $('#description'); + + element.onmouseup = event => { + frame.setAttribute('content', true); + + [loading, description] + .forEach(element => { + element.setAttribute('loading', true); + element.removeAttribute('style'); + }); + } + + frame.onload = frame.onerror = event => { + + [loading] + .forEach(element => { + element.setAttribute('loading', false); + setTimeout(() => element.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/plugn.js b/src/plugn.js index 9841ee9..d64b791 100644 --- a/src/plugn.js +++ b/src/plugn.js @@ -1,12 +1,17 @@ -/* Plugn.js (Plugin) - Web to Plex */ -/* global config */ +/* plugn.js (Plugin) - Web to Plex */ +/* global chrome */ -let KILL_DEBUGGER = false; +let PLUGN_DEVELOPER = false; -let logger = - KILL_DEBUGGER? - { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m }: - console; +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; function load(name) { return JSON.parse(localStorage.getItem(btoa(name))); @@ -16,32 +21,167 @@ function save(name, data) { return localStorage.setItem(btoa(name), JSON.stringify(data)); } -function GetConsent(origin) { - return load('permission:' + origin); +async function Load(name = '') { + if(!name) + return /* invalid name */; + + name = 'Cache-Data/' + btoa(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 locationchangecallbacks = []; +async function Save(name = '', data) { + if(!name) + return /* invalid name */; -function watchlocationchange(subject) { - watchlocationchange[subject] = watchlocationchange[subject] || location[subject]; + name = 'Cache-Data/' + btoa(name.toLowerCase().replace(/\s+/g, '')); + data = JSON.stringify(data); - if (watchlocationchange[subject] != location[subject]) { - watchlocationchange[subject] = location[subject]; + await PLUGN_STORAGE.set({[name]: data}, () => data); - for(let index = 0, length = locationchangecallbacks.length, callback; index < length; index++) { - callback = locationchangecallbacks[index]; + return name; +} - if(callback && typeof callback == 'function') - callback(new Event('locationchange', { bubbles: true })); +function GetConsent(name, builtin) { + return PLUGN_CONFIGURATION[`${ (builtin? 'builtin': 'plugin') }_${ name }`]; +} + +// get the saved options +function getConfiguration() { + return new Promise((resolve, reject) => { + function handleConfiguration(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.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, handleOptions); + 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.ExtensionBranchType) && !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 }); } -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 = []; @@ -51,20 +191,98 @@ function RandomName(length = 16, symbol = '') { return values.join(symbol).replace(/^[^a-z]+/i, ''); }; -let running = [], instance = RandomName(), TAB; +let handle = async(results, tabID, instance, script, type) => { + let InstanceWarning = `[${ type.toUpperCase() }:${ script }] Instance failed to execute @${ tabID }#${ instance }`, + InstanceType = type; + + 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 */; + + 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) { + chrome.tabs.sendMessage(tabID, { data, script, instance, 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 }; + + chrome.tabs.insertCSS(tabID, { file: 'sites/common.css' }); + chrome.tabs.sendMessage(tabID, { data, script, instance, instance_type: InstanceType, type: 'POPULATE' }); + } catch(error) { + throw new Error(InstanceWarning + ' - ' + String(error)); + } +}; + +let running = [], instance = RandomName(), TAB, cache = {}; -let tabchange = tabs => { - let tab = tabs[0]; +/* Handle script/plugin events */ +let tabchange = async tabs => { + let tab = tabs[0]; - if(!tab) return; + if(!tab || FOUND[instance]) return; TAB = tab; let id = tab.id, url = tab.url, - can, org, ali, js; - - if(!url || /^chrome/i.test(url) || (!!~running.indexOf(id) && !!~running.indexOf(instance))) + 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 @@ -72,58 +290,313 @@ let tabchange = tabs => { c) The tab AND instance are accounted for */; - url = new URL(url); - org = url.origin; - ali = url.host.replace(/^(ww\w+\.|\w{2}\.)/i, ''); - can = GetConsent(ali); - js = load(`script:${ ali }`); + 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(!can || !js) return; + if(!allowed || !js) return; - let name = (KILL_DEBUGGER? instance: `top.${instance }`); // makes debugging easier + 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)); + }); - fetch(`https://ephellon.github.io/web.to.plex/plugins/${ js }.js`, { mode: 'cors' }) + return setTimeout(() => cache = {}, 1e6); + } + + let name = (!PLUGN_DEVELOPER? instance: `top.${ instance }`); // makes debugging easier + + 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`; + + fetch(file, { 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)) + chrome.tabs.executeScript(id, { code: + (LAST = cache[ali] = +`/* ${ type }* (${ (!PLUGN_DEVELOPER? 'on':'off') }line) - "${ url.href }" */ +${ name } = (${ name } || (${ name }$ = $ => { +'use strict'; + +if(${ allowed } === false) + return ''; + +/* Start Injected */ +${ code } +/* End Injected */ + +let InjectedReadyState; + +top.addEventListener('popstate', ${ type }.init); +top.addEventListener('pushstate-changed', ${ type }.init); + +return (${ type }.RegExp = RegExp( + ${ type }.url + /*.replace(/\\|.*?(\\)|$)/g,'')*/ + .replace(/^\\*\\:/,'\\\\w{3,}:') + .replace(/\\*\\./g,'([^\\\\.]+\\\\.)?') + .replace(/\\/\\*/g,'/[^$]*'),'i') +).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 + ")"), 5000); +})(document.queryBy)); + +console.log('[${ name }]', ${ name }); + +top.onlocationchange = (event) => chrome.runtime.sendMessage({ type: '$INIT$', options: { ${ type }: '${ js }' } }, callback => callback); + +;${ 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 }); }; -window.onlocationchange = event => { - instance = RandomName(); - tabchange([TAB]); -}; +// listen for message event +let processMessage; -let handle = (results, tabID, instance, plugin) => { - let InstanceWarning = `Instance @${ tabID } [${ instance }] failed to execute`; +chrome.runtime.onMessage.addListener(processMessage = async(request, sender, callback) => { + let { options } = request, + tab = TAB || {}, + { id, url, href } = tab, + org; - if(!results || !results[0] || !instance) - return logger.warn(InstanceWarning); + 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 + */; - let data = results[0]; + url = new URL(url); + org = url.origin; - 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)); + let name = (!PLUGN_DEVELOPER? instance: `top.${ instance }`); // makes debugging easier + + 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`; + + switch(type) { + case 'PLUGIN': + allowed = await GetConsent(plugin, false); + + fetch(file, { 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: + (LAST = cache[plugin] = +`/* plugin (${ (!PLUGN_DEVELOPER? 'on':'off') }line) - "${ url.href }" */ +${ name } = (${ name } || (${ name }$ = $ => { +'use strict'; + +if(${ allowed } === false) + return ''; + +/* Start Injected */ +${ code } +/* End Injected */ + +let PluginReadyState; + +top.addEventListener('popstate', plugin.init); +top.addEventListener('pushstate-changed', plugin.init); + +return (plugin.RegExp = RegExp( + plugin.url + /*.replace(/\\|.*?(\\)|$)/g,'')*/ + .replace(/^\\*\\:/,'\\\\w{3,}:') + .replace(/\\*\\./g,'([^\\\\.]+\\\\.)?') + .replace(/\\/\\*/g,'/[^$]*'),'i') +).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 + ")"), 5000); +})(document.queryBy)); + +console.log('[${ name }]', ${ name }); + +top.onlocationchange = (event) => chrome.runtime.sendMessage({ type: '$INIT$', options: { plugin: '${ plugin }' } }, callback => callback); + +;${ 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); + + fetch(file, { 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: + (LAST = cache[script] = +`/* script (${ (!PLUGN_DEVELOPER? 'on':'off') }line) - "${ url.href }" */ +${ name } = (${ name } || (${ name }$ = $ => { +'use strict'; + +if(${ allowed } === false) + return ''; + +/* Start Injected */ +${ code } +/* End Injected */ + +let ScriptReadyState; + +top.addEventListener('popstate', script.init); +top.addEventListener('pushstate-changed', script.init); + +return (script.RegExp = RegExp( + script.url + // .replace(/\\|.*?(\\)|$)/g,'') + .replace(/^\\*\\:/,'\\\\w{3,}:') + .replace(/\\*\\./g,'([^\\\\.]+\\\\.)?') + .replace(/\\/\\*/g,'/[^$]*'),'i') +).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 + ")"), 5000); +})(document.queryBy)); + +console.log('[${ name }]', ${ name }); + +top.onlocationchange = (event) => chrome.runtime.sendMessage({ type: '$INIT$', options: { script: '${ script }' } }, callback => callback); + +;${ 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; + + case '_INIT_': + chrome.tabs.executeScript(id, { code: LAST }, results => handle(results, LAST_ID, LAST_INSTANCE, LAST_JS, LAST_TYPE)); + break; + + case '$INIT$': + chrome.tabs.getCurrent(tab => { + instance = RandomName(); + + setTimeout(() => tabchange([ tab ]), 5000); + }); + break; + + case 'FOUND': + FOUND[request.instance] = request.found; + break; + + default: + 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.html b/src/popup/index.html index f8adb7b..cb5512b 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -164,8 +164,8 @@ box-shadow: 0 10px 128px inset #6A8592; } - #gostream:hover { - box-shadow: 0 10px 128px inset #028CC9; + #vumoo:hover { + box-shadow: 0 10px 128px inset #DD1B2F; } #shana-project:hover { @@ -180,6 +180,30 @@ 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; } @@ -204,6 +228,10 @@ box-shadow: 0 10px 128px inset #E48F34; } + #local-medusa:hover { + box-shadow: 0 10px 128px inset #26B043; + } + [save-file]:after, [cost-cash-low]:after, [cost-cash-med]:after, [cost-cash-hig]:after { content: "____"; color: transparent; @@ -229,7 +257,7 @@ float: right; } - [is-shy] label:after { + [is-shy] label:after, [is-dead] label:after { content: " \1f910"; float: right; } @@ -238,6 +266,11 @@ 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; @@ -289,7 +322,7 @@ - + Verizon @@ -304,13 +337,13 @@ - + Shana Project - + YouTube @@ -325,27 +358,33 @@ + + + Vumoo + + + fandango - + Amazon + + + IMDb - - - CouchPotato @@ -358,33 +397,27 @@ + + + The MovieDb - - - Letterboxd - + Hulu - - - Flickmetrix - - - @@ -394,42 +427,48 @@ + + + Flickmetrix + + + + + + JustWatch + + + + + + iTunes - + showRSS - - - Vudu + + + Movieo - - - GoStream - - - - - - TV Maze @@ -442,12 +481,48 @@ - + + + + iTunes + + + Allocine + + + + + + MovieMeter + + + + + + + + + GoStream + + + + + + Tubi + + + + + + Web to Plex + + + diff --git a/src/popup/index.js b/src/popup/index.js index ac75a63..faf74de 100644 --- a/src/popup/index.js +++ b/src/popup/index.js @@ -55,7 +55,9 @@ document.body.onload = function() { "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 @@ -81,8 +83,10 @@ document.body.onload = function() { let elements = document.querySelectorAll(selectors.join(',')); - for(let element, index = 0, length = elements.length; index < length; index++) + 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 = `${ parse(messages[attribute], attribute, element) }. ${ element.title }`; + 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..ec296d3 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; @@ -302,6 +329,8 @@ position: fixed !important; z-index: 999999 !important; + min-height: 0 !important; + min-width: 0 !important; height: 72px !important; width: 180px !important; @@ -485,3 +514,88 @@ 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="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/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/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/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..b9590be 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,1694 +1,2124 @@ /* 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; + +(async date => { + + // default date items + let YEAR = date.getFullYear(), + MONTH = date.getMonth() + 1, + DATE = date.getDate(), + NOTIFIED = false; + + // simple helpers + let extURL = url => chrome.extension.getURL(url), + $ = (selector, container) => queryBy(selector, container); + + 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'), + }; -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'), -}; + // the storage - priority to sync + const UTILS_STORAGE = chrome.storage.sync || chrome.storage.local; -// the custom "on location change" event -let locationchangecallbacks = []; + async function load(name = '') { + if(!name) + return /* invalid name */; -function watchlocationchange(subject) { - watchlocationchange[subject] = watchlocationchange[subject] || location[subject]; + name = 'Cache-Data/' + btoa(name.toLowerCase().replace(/\s+/g, '')); - if (watchlocationchange[subject] != location[subject]) { - watchlocationchange[subject] = location[subject]; + return new Promise((resolve, reject) => { + function LOAD(DISK) { + let data = JSON.parse(DISK[name] || null); - for(let index = 0, length = locationchangecallbacks.length, callback; index < length; index++) { - callback = locationchangecallbacks[index]; + return resolve(data); + } - if(callback && typeof callback == 'function') - callback(new Event('locationchange', { bubbles: true })); - } + UTILS_STORAGE.get(null, DISK => { + if(chrome.runtime.lastError) + chrome.storage.local.get(null, LOAD); + else + LOAD(DISK); + }); + }); } -} -Object.defineProperty(window, 'onlocationchange', { - set: callback => locationchangecallbacks.push(callback) -}); + async function save(name = '', data) { + if(!name) + return /* invalid name */; -setInterval(() => watchlocationchange('pathname'), 1000); // at least 1s is needed to properly fire the event ._. + name = 'Cache-Data/' + btoa(name.toLowerCase().replace(/\s+/g, '')); + data = JSON.stringify(data); -// the storage -const storage = chrome.storage.sync || chrome.storage.local; + // erase entries after 400-500 have been made + UTILS_STORAGE.get(null, items => { + let array = [], bytes = 0; -async function load(name = '') { - if(!name) return; + for(let item in items) { + let object = items[item]; - name = 'Cache-Data/' + btoa(name.toLowerCase().replace(/\s+/g, '')); + array.push(item); + bytes += (typeof object == 'string'? object.length * 8: typeof object == 'boolean'? 8: JSON.stringify(object).length * 8)|0; + } - return new Promise((resolve, reject) => { - function LOAD(DISK) { - let data = JSON.parse(DISK[name] || null); + if((UTILS_STORAGE.MAX_ITEMS && array.length >= UTILS_STORAGE.MAX_ITEMS) || bytes >= UTILS_STORAGE.QUOTA_BYTES) + for(let item in items) + if(/^cache-data\//i.test(item)) + UTILS_STORAGE.remove(item); + }); - return resolve(data); - } + await UTILS_STORAGE.set({[name]: data}, () => data); - storage.get(null, DISK => { - if (chrome.runtime.lastError) - chrome.storage.local.get(null, LOAD); - else - LOAD(DISK); - }); - }); -} + return name; + } -async function save(name = '', data) { - if(!name) return; + async function remove(name) { + if(!name) + return /* invalid name */; - name = 'Cache-Data/' + btoa(name.toLowerCase().replace(/\s+/g, '')); - data = JSON.stringify(data); + return await UTILS_STORAGE.remove(['Cache-Data/' + btoa(name.toLowerCase().replace(/\s+/g, ''))]); + } - await storage.set({[name]: data}, () => 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]; + + if(((state == 'error' || state == 'warning') && configuration.NotifyNewOnly && /\balready\s+(exists?|(been\s+)?added)\b/.test(text)) || (configuration.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]; + } + } - return name; -} + 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( + configuration.usingRadarr? + configuration.radarrQualities: + configuration.usingWatcher? + configuration.watcherQualities: + '[]' + ), + show: JSON.parse( + configuration.usingSonarr? + configuration.sonarrQualities: + configuration.usingMedusa? + configuration.medusaQualities: + '[]' + ) + }, + locations = { + movie: JSON.parse( + configuration.usingRadarr? + configuration.radarrStoragePaths: + configuration.usingWatcher? + configuration.watcherStoragePaths: + '[]' + ), + show: JSON.parse( + configuration.usingSonarr? + configuration.sonarrStoragePaths: + configuration.usingMedusa? + configuration.medusaStoragePaths: + '[]' + ) + }, + defaults = { + movie: ( + configuration.usingRadarr? + { quality: configuration.__radarrQuality, location: configuration.__radarrStoragePath }: + {} + ), + show: ( + configuration.usingSonarr? + { quality: configuration.__sonarrQuality, location: configuration.__sonarrStoragePath }: + configuration.usingMedusa? + { quality: configuration.__medusaQuality, location: configuration.__medusaStoragePath }: + {} + ) + }; -async function kill(name) { - return storage.remove(['Cache-Data/' + btoa(name.toLowerCase().replace(/\s+/g, ''))]); -} + 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() } }), + ( + configuration.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))): + '' + ),( + configuration.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; + } -// 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]; + 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() } }), + ( + configuration.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))): + '' + ),( + configuration.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; + } - if (last && last.done === false) - return (last => setTimeout(() => new Notification(state, text, timeout, callback, requiresClick), +(new Date) - last.start))(last); + return elements + })(array) + ), - let element = document.furnish(`div.web-to-plex-notification.${state}`, { - onclick: event => { - let notification = Notification.queue[event.target.id], - element = notification.element; + // 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}`: '/' }` }), + ( + configuration.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'), + ( + configuration.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') + ) + ) + ); - notification.done = true; - Notification.queue.list.splice(notification.index, 1); - clearTimeout(notification.job); - element.remove(); + if(P_QUA) P_QUA.value = defaults[type].quality; + if(P_LOC) P_LOC.value = defaults[type].location; - let removed = delete Notification.queue[notification.id]; + P_QUA = P_LOC = null; + break; - return (event.requiresClick)? null: notification.callback(removed); + default: + return UTILS_TERMINAL.warn(`Unknown prompt type "${ prompt_type }"`); + break; } - }, 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 container.append(prompt), prompt; + } + } - return queue[element.id]; + // open up the options page + function Options() { + chrome.runtime.sendMessage({ + type: 'OPEN_OPTIONS' + }); } -} -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(); + // Send an update query to background.js + Update = (type, options = {}, postToo) => { + if(configuration) + UTILS_TERMINAL.log(`Requesting update: ${ type }`, options); - data.splice(+element.value, 1, null); - header.innerText = 'Approve ' + counter.children.length + (counter.children.length == 1?' item': ' items'); - }; + chrome.runtime.sendMessage({ + type, + options + }); - 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; - } + if(postToo) + top.postMessage(options); + }; - 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; + // get the saved options + function options() { + 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; + } - /* 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(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.usingOmbi && o.ombiURLRoot && o.ombiToken) { + o.ombiURL = o.ombiURLRoot; + } else { + delete o.ombiURL; // prevent variable ghosting + } - if(element === true) - return prompter.remove(); - else - element.remove(); + if(o.usingCouchPotato && o.couchpotatoURLRoot && o.couchpotatoToken) { + o.couchpotatoURL = `${ items.couchpotatoURLRoot }/api/${encodeURIComponent(o.couchpotatoToken)}`; + } else { + delete o.couchpotatoURL; // prevent variable ghosting + } - data.splice(+element.value, 1, null); - header.innerText = 'Approve ' + counter.children.length + (counter.children.length == 1?' item': ' items'); - }; + if(o.usingWatcher && o.watcherURLRoot && o.watcherToken) { + o.watcherURL = o.watcherURLRoot; + } else { + delete o.watcherURL; // prevent variable ghosting + } - 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; + if(o.usingRadarr && o.radarrURLRoot && o.radarrToken) { + o.radarrURL = o.radarrURLRoot; + } else { + delete o.radarrURL; // prevent variable ghosting + } - default: - return terminal.warn(`Unknown prompt type "${ prompt_type }"`); - break; - } + if(o.usingSonarr && o.sonarrURLRoot && o.sonarrToken) { + o.sonarrURL = o.sonarrURLRoot; + } else { + delete o.sonarrURL; // prevent variable ghosting + } - return container.append(prompt), prompt; - } -} + if(o.usingMedusa && o.medusaURLRoot && o.medusaToken) { + o.medusaURL = o.medusaURLRoot; + } else { + delete o.medusaURL; // prevent variable ghosting + } -// Send an update query to background.js -function sendUpdate(type, options = {}) { - terminal.log(`Requesting update: ${ type }`, options); + resolve(o); + } - chrome.runtime.sendMessage({ - type, - options - }); -} + UTILS_STORAGE.get(null, options => { + if(chrome.runtime.lastError) + chrome.storage.local.get(null, handleOptions); + else + handleOptions(options); + }); + }); + } -// 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 - }; + // self explanatory, returns an object; sets the configuration variable + function ParsedOptions() { + return options() + .then( + options => (configuration = options), + error => { + new Notification( + 'warning', + 'Fill in missing Web to Plex options', + 15000, + Options + ); + throw error; + } + ); + } - options.plexURL = o.plexURL? - `${ o.plexURL }web#!/server/${ o.server.id }/`: - `https://app.plex.tv/web/app#!/server/${ o.server.id }/`; - } else { - o = options; - } + await ParsedOptions(); - if (o.couchpotatoBasicAuthUsername) - o.couchpotatoBasicAuth = { - username: o.couchpotatoBasicAuthUsername, - password: o.couchpotatoBasicAuthPassword - }; + let AUTO_GRAB = { + ENABLED: configuration.UseAutoGrab, + LIMIT: configuration.AutoGrabLimit, + }, + UTILS_DEVELOPER = configuration.ExtensionBranchType, // = { 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 }; - // TODO: stupid copy/pasta - if (o.watcherBasicAuthUsername) - o.watcherBasicAuth = { - username: o.watcherBasicAuthUsername, - password: o.watcherBasicAuthPassword - }; + UTILS_TERMINAL.log('UTILS_DEVELOPER:', UTILS_DEVELOPER, configuration); - if (o.radarrBasicAuthUsername) - o.radarrBasicAuth = { - username: o.radarrBasicAuthUsername, - password: o.radarrBasicAuthPassword - }; + // parse the formatted headers and URL + function HandleProxyHeaders(Headers = "", URL = "") { + let headers = {}; - if (o.sonarrBasicAuthUsername) - o.sonarrBasicAuth = { - username: o.sonarrBasicAuthUsername, - password: o.sonarrBasicAuthPassword - }; + Headers.replace(/^[ \t]*([^\=\s]+)[ \t]*=[ \t]*((["'`])(?:[^\\\3]*|\\.)\3|[^\f\n\r\v]*)/gm, ($0, $1, $2, $3, $$, $_) => { + let string = !!$3; - if (o.ombiURLRoot && o.ombiToken) { - o.ombiURL = o.ombiURLRoot; + if(string) { + headers[$1] = $2.replace(RegExp(`^${ $3 }|${ $3 }$`, 'g'), ''); } else { - delete o.ombiURL; // prevent variable ghosting - } + $2 = $2.replace(/@([\w\.]+)/g, (_0, _1, _$, __) => { + let path = _1.split('.'), property = top; - if (o.couchpotatoURLRoot && o.couchpotatoToken) { - o.couchpotatoURL = `${ items.couchpotatoURLRoot }/api/${encodeURIComponent(o.couchpotatoToken)}`; - } else { - delete o.couchpotatoURL; // prevent variable ghosting - } + for(let index = 0, length = path.length; index < length; index++) + property = property[path[index]]; - if (o.watcherURLRoot && o.watcherToken) { - o.watcherURL = o.watcherURLRoot; - } else { - delete o.watcherURL; // prevent variable ghosting + headers[$1] = property; + }) + .replace(/@\{b(ase-?)?64-url\}/gi, btoa(URL)) + .replace(/@\{enc(ode)?-url\}/gi, encodeURIComponent(URL)) + .replace(/@\{(raw-)?url\}/gi, URL); } + }); - if (o.radarrURLRoot && o.radarrToken) { - o.radarrURL = o.radarrURLRoot; - } else { - delete o.radarrURL; // prevent variable ghosting - } + return headers; + } - if (o.sonarrURLRoot && o.sonarrToken) { - o.sonarrURL = o.sonarrURLRoot; - } else { - delete o.sonarrURL; // prevent variable ghosting - } + // 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: configuration.TMDbAPI || 'bcb95f026f9a01ffa707fcff71900e94', + omdb: configuration.OMDbAPI || 'PlzBanMe', + ombi: configuration.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 = configuration.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 && (configuration.usingOmbi || (configuration.usingRadarr && rqut == 'tmdb') || ((configuration.usingSonarr || configuration.usingMedusa) && 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); + } - resolve(o); + if(local) { + UTILS_TERMINAL.log('[LOCAL] Search results', local); + return local; } - storage.get(null, options => { - if (chrome.runtime.lastError) - chrome.storage.local.get(null, handleOptions); - else - handleOptions(options); - }); - }); -} + /* the rest of this function is a beautiful mess that will need to be dealt with later... but it works */ + let url = + (manable && title && configuration.usingOmbi)? + `${ configuration.ombiURLRoot }api/v1/Search/${ (rqut == 'imdb' || rqut == 'tmdb' || apit == 'movie')? 'movie': 'tv' }/${ plus(title, '%20') }/?apikey=${ api.ombi }`: + (manable && (configuration.usingRadarr || configuration.usingSonarr || configuration.usingMedusa))? + (configuration.usingRadarr && (rqut == 'imdb' || rqut == 'tmdb'))? + (mid)? + `${ configuration.radarrURLRoot }api/movie/lookup/tmdb?tmdbId=${ mid }&apikey=${ configuration.radarrToken }`: + (iid)? + `${ configuration.radarrURLRoot }api/movie/lookup/imdb?imdbId=${ iid }&apikey=${ configuration.radarrToken }`: + `${ configuration.radarrURLRoot }api/movie/lookup?term=${ plus(title, '%20') }&apikey=${ configuration.radarrToken }`: + (configuration.usingSonarr)? + (tid)? + `${ configuration.sonarrURLRoot }api/series/lookup?term=tvdb:${ tid }&apikey=${ configuration.sonarrToken }`: + `${ configuration.sonarrURLRoot }api/series/lookup?term=${ plus(title, '%20') }&apikey=${ configuration.sonarrToken }`: + (configuration.usingMedusa)? + (tid)? + `${ configuration.medusarURLRoot }api/v2/series/tvdb${ tid }?detailed=true&${ tid }&api_key=${ configuration.medusaToken }`: + `${ configuration.medusaURLRoot }api/v2/internal/searchIndexersForShowName?query=${ plus(title) }&indexerId=0&api_key=${ configuration.medusaToken }`: + 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 = configuration.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 }); + } -// self explanatory -function openOptionsPage() { - chrome.runtime.sendMessage({ - type: 'OPEN_OPTIONS' - }); -} + UTILS_TERMINAL.log(`Searching for "${ title } (${ year })" in ${ type || apit }/${ rqut }${ proxy.enabled? '[PROXY]': '' } => ${ url }`); -// 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 - ); + 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; - } - ); -} + }); -let config = parseOptions(), - AUTO_GRAB = { - ENABLED: config.UseAutoGrab, - LIMIT: config.AutoGrabLimit, - }; + UTILS_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(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]); + } -function HandleProxyHeaders(Headers = "", URL = "") { - let headers = {}; + 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 = configuration.UseLooseScore | 0; - Headers.replace(/^[ \t]*([^\=\s]+)[ \t]*=[ \t]*((["'`])(?:[^\\\3]*|\\.)\3|[^\f\n\r\v]*)/gm, ($0, $1, $2, $3, $$, $_) => { - let string = !!$3; + 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 }%`); - if(string) { - headers[$1] = $2.replace(RegExp(`^${ $3 }|${ $3 }$`, 'g'), ''); - } else { - $2 = $2.replace(/@([\w\.]+)/g, (_0, _1, _$, __) => { - let path = _1.split('.'), property = top; + 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(configuration.usingMedusa && $data instanceof Array) + found = ((t($data[4]) == t(title) || $alt) && +year === +$data[5].slice(0, 4))? + $alt || $data: + found; + // Radarr & Sonarr + else if(configuration.usingRadarr || configuration.usingSonarr) + 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; - for(let index = 0, length = path.length; index < length; index++) - property = property[path[index]]; + UTILS_TERMINAL.log(`Strict Matching: ${ !!found }`, !!found? found: null); + } - headers[$1] = property; - }) - .replace(/@\{b(ase-?)?64-url\}/gi, btoa(URL)) - .replace(/@\{enc(ode)?-url\}/gi, encodeURIComponent(URL)) - .replace(/@\{(raw-)?url\}/gi, URL); - } - }); + // 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(configuration.usingMedusa && $data instanceof Array) + found = (c($data[4]) == c(title) || $alt)? + $alt || $data: + found; + // Radarr & Sonarr + if(configuration.usingRadarr || configuration.usingSonarr) + 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; - return headers; -} + UTILS_TERMINAL.log(`Title Matching: ${ !!found }`, !!found? found: null); + } -// 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); - } + // 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; configuration.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(configuration.usingMedusa && $data instanceof Array) + found = (R($data[4], title) || $alt)? + $alt || $data: + found; + // Radarr & Sonarr + if(configuration.usingRadarr || configuration.usingSonarr) + found = (R($data.name || $data.title, 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; - if(local) { - terminal.log('[LOCAL] Search results', local); - return local; + 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 && (!configuration.usingMedusa? true: (configuration.usingSonarr || configuration.usingOmbi))) + json = json[0]; + + if(!json) + json = { IMDbID, TMDbID, TVDbID }; + + // Ombi, Medusa, Radarr and Sonarr + if(manable) + data = ( + (configuration.usingMedusa && !(configuration.usingSonarr || configuration.usingOmbi))? + { + 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; } - /* 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 }); + 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: `${ configuration.couchpotatoURL }/media.get`, + IMDbID: options.IMDbID, + TMDbID: options.TMDbID, + TVDbID: options.TVDbID, + basicAuth: configuration.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', + `Movie already exists in CouchPotato (status: ${response.status})` + ); + } + ); } - terminal.log(`Searching for "${ title } (${ year })" in ${ type || apit }/${ rqut }${ proxy.enabled? '[PROXY]': '' } => ${ url }`); + // Movies/TV Shows + function Request_Ombi(options) { + new Notification('info', `Adding "${ options.title }" to Ombi`, 3000); - 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; - }); + if((!options.IMDbID && !options.TMDbID) && !options.TVDbID) { + return new Notification( + 'warning', + 'Stopped adding to Ombi: No content ID' + ); + } - terminal.log('Search results', { title, year, url, json }); + let contentType = (/movies?|film/i.test(options.type)? 'movie': 'tv'); - if('results' in json) - json = json.results; + chrome.runtime.sendMessage({ + type: 'PUSH_OMBI', + url: `${ configuration.ombiURL }api/v1/Request/${ contentType }`, + token: configuration.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(); + + UTILS_TERMINAL.log('Successfully pushed', options); + new Notification('update', `Added "${ options.title }" to Ombi`, 7000, () => window.open(configuration.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))); + } + } + ); + } - 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 = "") => { + // Movies/TV Shows + function Request_CouchPotato(options) { + new Notification('info', `Adding "${ options.title }" to CouchPotato`, 3000); + + chrome.runtime.sendMessage( + { + type: 'PUSH_COUCHPOTATO', + url: `${ configuration.couchpotatoURL }/movie.add`, + IMDbID: options.IMDbID, + TMDbID: options.TMDbID, + TVDbID: options.TVDbID, + basicAuth: configuration.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) { + UTILS_TERMINAL.log('Successfully pushed', options); + new Notification('update', `Added "${ options.title }" to CouchPotato`); + } else { + new Notification('warning', `Could not add "${ options.title }" to CouchPotato`); + } + } + ); + } - 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, '|'] - ]; + // Movies + function Request_Watcher(options) { + new Notification('info', `Adding "${ options.title }" to Watcher`, 3000); - for(let i = 0; i < r.length; i++) { - if(/^([\(\|\)]+)?$/.test(s)) return ""; + if(!options.IMDbID && !options.TMDbID) { + return new Notification( + 'warning', + 'Stopped adding to Watcher: No IMDb/TMDb ID' + ); + } - s = s.replace(r[i][0], r[i][1]); + chrome.runtime.sendMessage({ + type: 'PUSH_WATCHER', + url: `${ configuration.watcherURL }api/`, + token: configuration.watcherToken, + StoragePath: configuration.watcherStoragePath, + basicAuth: configuration.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; + + UTILS_TERMINAL.log('Successfully pushed', options); + new Notification('update', `Added "${ options.title }" to Watcher`, 7000, () => window.open(`${configuration.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))); } + } + ); + } - 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; + // 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; - 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); - } + let PromptValues = {}, + { PromptQuality, PromptLocation } = configuration; - // 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); - } + if(!prompted && (PromptQuality || PromptLocation)) + return new Prompt('modify', options, refined => Request_Radarr(refined, true)); - // 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); - } + if(PromptQuality && +options.quality > 0) + PromptValues.QualityID = +options.quality; + if(PromptLocation && options.location) + PromptValues.StoragePath = JSON.parse(configuration.radarrStoragePaths).map(item => item.id == options.location? item: null).filter(n => n)[0].path.replace(/\\/g, '\\\\'); - json = found; + new Notification('info', `Adding "${ options.title }" to Radarr`, 3000); + + chrome.runtime.sendMessage({ + type: 'PUSH_RADARR', + url: `${ configuration.radarrURL }api/movie/`, + token: configuration.radarrToken, + StoragePath: configuration.radarrStoragePath, + QualityID: configuration.radarrQualityProfileId, + basicAuth: configuration.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; + + UTILS_TERMINAL.log('Successfully pushed', options); + new Notification('update', `Added "${ options.title }" to Radarr`, 7000, () => window.open(`${configuration.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((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 }; + // TV Shows + function Request_Sonarr(options, prompted) { + if(!options.TVDbID) + return (!prompted)? new Notification( + 'warning', + 'Stopped adding to Sonarr: No TVDb ID' + ): null; - let ei = 'tt', - mr = 'movie_results', - tr = 'tv_results'; + let PromptValues = {}, + { PromptQuality, PromptLocation } = configuration; - json = json && mr in json? json[mr].length > json[tr].length? json[mr]: json[tr]: json; + if(!prompted && (PromptQuality || PromptLocation)) + return new Prompt('modify', options, refined => Request_Sonarr(refined, true)); - if(json instanceof Array) - json = json[0]; + if(PromptQuality && +options.quality > 0) + PromptValues.QualityID = +options.quality; + if(PromptLocation && options.location) + PromptValues.StoragePath = JSON.parse(configuration.sonarrStoragePaths).map(item => item.id == options.location? item: null).filter(n => n)[0].path.replace(/\\/g, '\\\\'); - if(!json) - json = { IMDbID, TMDbID, TVDbID }; + new Notification('info', `Adding "${ options.title }" to Sonarr`, 3000); - // 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 - }; + chrome.runtime.sendMessage({ + type: 'PUSH_SONARR', + url: `${ configuration.sonarrURL }api/series/`, + token: configuration.sonarrToken, + StoragePath: configuration.sonarrStoragePath, + QualityID: configuration.sonarrQualityProfileId, + basicAuth: configuration.sonarrBasicAuth, + title: options.title, + year: options.year, + tvdbId: options.TVDbID, + ...PromptValues + }, + response => { + UTILS_TERMINAL.log('Pushing to Sonarr', response); - year = +((data.year + '').slice(0, 4)) || 0; - data.year = year; + 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(); - let best = { title, year, data, type, rqut, score: json.score | 0 }; + UTILS_TERMINAL.log('Successfully pushed', options); + new Notification('update', `Added "${ options.title }" to Sonarr`, 7000, () => window.open(`${configuration.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))); + } + } + ); + } - terminal.log('Best match:', url, { best, json }); + // TV Shows + function Request_Medusa(options, prompted) { + if(!options.TVDbID) + return (!prompted)? new Notification( + 'warning', + 'Stopped adding to Medusa: No TVDb ID' + ): null; - if(best.data.imdb == ei && best.data.tmdb == 0 && best.data.tvdb == 0) - return terminal.log(`No information was found for "${ title } (${ year })"`), {}; + let PromptValues = {}, + { PromptQuality, PromptLocation } = configuration; - 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); + if(!prompted && (PromptQuality || PromptLocation)) + return new Prompt('modify', options, refined => Request_Medusa(refined, true)); - terminal.log(`Saved as "${ savename }"`, data); + if(PromptQuality && +options.quality > 0) + PromptValues.QualityID = +options.quality; + if(PromptLocation && options.location) + PromptValues.StoragePath = JSON.parse(configuration.medusaStoragePaths).map(item => item.id == options.location? item: null).filter(n => n)[0].path.replace(/\\/g, '\\\\'); - return data; -} + new Notification('info', `Adding "${ options.title }" to Medusa`, 3000); -function $pushAddToCouchpotato(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 && terminal.error('Error viewing CouchPotato: ' + String(response.error))); - } - if (!movieExists) { - pushCouchPotatoRequest(options); - return; - } - new Notification( - 'warning', - `Movie is already in CouchPotato (status: ${response.status})` - ); - } - ); -} + chrome.runtime.sendMessage({ + type: 'PUSH_MEDUSA', + url: `${ configuration.medusaURL }api/v2/series`, + root: `${ configuration.medusaURL }api/v2/`, + token: configuration.medusaToken, + StoragePath: configuration.medusaStoragePath, + QualityID: configuration.medusaQualityProfileId, + basicAuth: configuration.medusaBasicAuth, + title: options.title, + year: options.year, + tvdbId: options.TVDbID, + ...PromptValues + }, + response => { + UTILS_TERMINAL.log('Pushing to Medusa', response); -// Movies/TV Shows -function pushOmbiRequest(options) { - new Notification('info', `Adding "${ options.title }" to Ombi`, 3000); + 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(); - if ((!options.IMDbID && !options.TMDbID) && !options.TVDbID) { - return new Notification( - 'warning', - 'Stopped adding to Ombi: No content ID' + UTILS_TERMINAL.log('Successfully pushed', options); + new Notification('update', `Added "${ options.title }" to Medusa`, 7000, () => window.open(`${configuration.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))); + } + } ); } - 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); + // make the button + let MASTER_BUTTON; + function RenderButton(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; + + // -// TV Shows -function pushSonarrRequest(options) { - new Notification('info', `Adding "${ options.title }" to Sonarr`, 3000); + document.body.appendChild(button); - if (!options.TVDbID) { - return new Notification( - 'warning', - 'Stopped adding to Sonarr: No TVDb ID' - ); + return MASTER_BUTTON = button; } - 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); + function UpdateButton(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; + }; - 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(); + Update('SEARCH_FOR', { ...options, button }); - 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))); - } - } - ); -} + /* Handle a list of items */ + if(multiple) { + options = [].slice.call(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; - - // + if(!options || !options.type || !options.title) return; - document.body.appendChild(button); + let empty = (em.test(options.IMDbID) && em.test(options.TMDbID) && em.test(options.TVDbID)), + nice_title = `${options.title.toCaps()}${options.year? ` (${options.year})`: ''}`; - return button; -} + if(options) { + ty = (options.type == 'movie'? 'Movie': 'TV Show'); + txt = options.txt || txt; + hov = options.hov || hov; + } -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; - }; + if(action == 'found') { + element.href = Request_PlexURL(configuration.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 'oload': + case 'consistent': + let href = options.href, path = ''; + + if(configuration.usingOmbi) { + path = ''; + } else if(configuration.usingWatcher && !tv.test(options.type)) { + path = ''; + } else if(configuration.usingRadarr && !tv.test(options.type)) { + path = configuration.radarrStoragePath; + } else if(configuration.usingSonarr && tv.test(options.type)) { + path = configuration.sonarrStoragePath; + } else if(configuration.usingMedusa && tv.test(options.type)) { + path = configuration.medusaStoragePath; + } else if(configuration.usingCouchPotato) { + path = ''; + } - sendUpdate('SEARCH_FOR', { ...options, button }); + 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(); - /* Handle a list of items */ - if(multiple) { - options = [].slice.call(options); + Update('DOWNLOAD_FILE', { ...options, button, href, path }); + new Notification('update', 'Opening prompt (may take a while)...'); + }); - let saved_options = [], // a list of successful searches (not on Plex) - len = options.length, - s = (len == 1? '': 's'), - t = []; + 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(); + if(configuration.usingOmbi) { + Request_Ombi(options); + } else if(configuration.usingWatcher && !tv.test(options.type)) { + Request_Watcher(options); + } else if(configuration.usingRadarr && !tv.test(options.type)) { + Request_Radarr(options); + } else if(configuration.usingSonarr && tv.test(options.type)) { + Request_Sonarr(options); + } else if(configuration.usingMedusa && tv.test(options.type)) { + Request_Medusa(options); + } else if(configuration.usingCouchPotato) { + __Request_CouchPotato__(options); + } + }); + } + NOTIFIED = false; - for(let index = 0; index < len; index++) { - let option = options[index]; + element.setAttribute(hov, `Add "${ nice_title }" | ${ty}`); + element.style.removeProperty('display'); + } else if(action == 'notfound' || action == 'error' || empty) { + element.removeAttribute('href'); - // Skip empty entries - if(!option || !option.type || !option.title) continue; + empty = !(options && options.title); - // 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 }`; + if(empty) + element.setAttribute(hov, `${ty || 'Item'} not found`); + else + element.setAttribute(hov, `"${ nice_title }" was not found`); - /* Failed */ - if(/#tt-0-0/i.test(url)) - continue; + button.classList.remove('wtp--found'); + button.classList.add('wtp--error'); + } - saved_options.push(option); - t.push(option.title); + element.id = options? `${options.IMDbID || 'tt'}-${options.TMDbID | 0}-${options.TVDbID | 0}`: 'tt-0-0'; } + } - t = t.join(', '); - t = t.length > 24? t.slice(0, 21).replace(/\W+$/, '') + '...': t; - - element.ON_CLICK = e => { - e.preventDefault(); + // Find media on Plex + async function FindMediaItems(options, button) { + if(!(options && options.length && button)) + return; - let self = e.target, tv = /tv[\s-]?|shows?|series/i, fail = 0, - options = JSON.parse(atob(button.getAttribute('saved_options'))); + let results = [], + length = options.length, + queries = (FindMediaItems.queries = FindMediaItems.queries || {}); - for(let index = 0, length = options.length, option; index < length; index++) { - option = options[index]; + FindMediaItems.OPTIONS = options; - 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 })`) - } - } + let query = JSON.stringify(options); - if (fail) - new Notification('error', `Failed to grab ${ fail } item${fail==1?'':'s'}`); - }; + query = (queries[query] = queries[query] || {}); - 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) })); + if(query.running === true) + return; + else if(query.results) { + let { results, multiple, items } = query; - 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 */ + new Notification('update', `Welcome back. ${ multiple } new ${ items } can be grabbed`, 7000, (event, target = button.querySelector('.list-action')) => target.click({ ...event, target })); - if(!options || !options.type || !options.title) return; + if(multiple) + UpdateButton(button, 'multiple', `Download ${ multiple } ${ items }`, results); - 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; + return; } - 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 = ''; - } + query.running = true; - 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(); + new Notification('info', `Processing ${ length } item${ 's'[+(length === 1)] || '' }...`); - sendUpdate('DOWNLOAD_FILE', { ...options, button, href, path }); - new Notification('update', 'Opening prompt (may take a while)...'); - }); + for(let index = 0, option, opt; index < length; index++) { + let { IMDbID, TMDbID, TVDbID } = (option = await options[index]); - 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; + 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'; - /* 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); + return await Request_Plex(option) + .then(({ found, key }) => { + if(found) { + // ignore found items, we only want new items + } else { + let available = (configuration.usingOmbi || configuration.usingWatcher || configuration.usingRadarr || configuration.usingSonarr || configuration.usingMedusa || configuration.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)); } + } - element.setAttribute(hov, `Add "${ nice_title }" | ${ty}`); - element.style.removeProperty('display'); - } else if (action == 'notfound' || action == 'error' || empty) { - element.removeAttribute('href'); + results = results.filter(v => v.status == 'downloader'); - empty = !(options && options.title); + 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: btoa(JSON.stringify(results)) }, img), + op = document.querySelector('#wtp-plexit'); - if(empty) - element.setAttribute(hov, `${ty || 'Item'} not found`); - else - element.setAttribute(hov, `"${ nice_title }" was not found`); + if(po = button.querySelector('#plexit')) + po.remove(); + try { + button.querySelector('ul').insertBefore(pi, op); + } catch(e) { /* Don't do anything */ } - button.classList.remove('wtp--found'); - button.classList.add('wtp--error'); - } + let multiple = results.length, + items = multiple == 1? 'item': 'items'; - element.id = options? `${options.IMDbID || 'tt'}-${options.TMDbID | 0}-${options.TVDbID | 0}`: 'tt-0-0'; - } -} + new Notification('update', `Done. ${ multiple } new ${ items } can be grabbed`, 7000, (event, target = button.querySelector('.list-action')) => target.click({ ...event, target })); -async function squabblePlex(options, button) { - if(!(options && options.length && button)) - return; + query.running = false; + query.results = results; + query.multiple = multiple; + query.items = items; - let results = [], - length = options.length; + if(multiple) + UpdateButton(button, 'multiple', `Download ${ multiple } ${ items }`, results); + } + + async function FindMediaItem(options) { + if(!(options && options.title)) + return; - squabblePlex.OPTIONS = options; + let { IMDbID, TMDbID, TVDbID } = options; - new Notification('info', `Processing ${ length } item${ 's'[+(length === 1)] || '' }...`); + TMDbID = +TMDbID; + TVDbID = +TVDbID; - for(let index = 0, option, opt; index < length; index++) { - let { IMDbID, TMDbID, TVDbID } = (option = await options[index]); + 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'))} }); - 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 }; + FindMediaItem.OPTIONS = options; try { - await getPlexMediaRequest(option) - .then(async({ found, key }) => { - if (found) { - // ignore found items, we only want new items + 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: btoa(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 { - option.field = 'original_title'; + options.field = 'original_title'; - return await getPlexMediaRequest(option) + return Request_Plex(options) .then(({ found, key }) => { - if (found) { - // ignore found items, we only want new items + 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: btoa(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.ombiURL || config.watcherURL || config.radarrURL || config.sonarrURL || config.couchpotatoURL), + let available = (configuration.usingOmbi || configuration.usingWatcher || configuration.usingRadarr || configuration.usingSonarr || configuration.usingMedusa || configuration.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 }); + UpdateButton(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)) + try { + options.button.querySelector('ul').insertBefore(pi, op); + } catch(e) { /* Don't do anything */ } } + return found; }); } + return found; }) - } catch(error) { - terminal.error('Request to Plex failed: ' + String(error)); - // new Notification('error', 'Failed to query item #' + (index + 1)); - } + } 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'); + } } - results = results.filter(v => v.status == 'downloader'); + function Request_Plex(options) { + if(!(configuration.plexURL && configuration.plexToken) || configuration.DO_NOT_USE) + return new Promise((resolve, reject) => resolve({ found: false, key: null })); - 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'); + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage({ + type: 'SEARCH_PLEX', + options, + serverConfig: configuration.server + }, + response => + (response && response.error)? + reject(response.error): + (!response)? + reject(new Error('Unknown error')): + resolve(response) + ); + }); + } - if(po = button.querySelector('#plexit')) - po.remove(); - button.querySelector('ul').insertBefore(pi, op); + function Request_PlexURL(PlexUIID, key) { + return configuration.plexURL.replace(RegExp(`\/(${ configuration.server.id })?$`), `/web#!/server/` + PlexUIID) + `/details?key=${encodeURIComponent( key )}`; + } - let multiple = results.length, - items = multiple == 1? 'item': 'items'; + /* Listen for events */ + chrome.runtime.onMessage.addListener(async(request, sender) => { + UTILS_TERMINAL.log(`Listener event [${ request.instance_type }#${ request[request.instance_type.toLowerCase()] }]:`, request); - new Notification('update', `Done. ${ multiple } new ${ items } can be grabbed`, 7000, (event, target = button.querySelector('.list-action')) => target.click({ ...event, target })); + 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 (multiple) - modifyPlexButton(button, 'multiple', `Download ${ multiple } ${ items }`, results); -} + if(!data) + return UTILS_TERMINAL.warn(EMPTY_REQUEST); + let button = RenderButton(); -function findPlexMedia(options) { - if(!(options && options.title)) - return; + if(!button) + return UTILS_TERMINAL.warn(BUTTON_ERROR); - let { IMDbID, TMDbID, TVDbID } = options; + switch(request.type) { + case 'POPULATE': - TMDbID = +TMDbID; - TVDbID = +TVDbID; + 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); - 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'))} }); + data = data.filter(value => value !== null && value !== undefined); - findPlexMedia.OPTIONS = options; + for(let index = 0, length = data.length, item; index < length; index++) { + let { image, type, title, year, IMDbID, TMDbID, TVDbID } = (item = data[index]); - 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' }; + if(!item.title || !item.type) + continue; - let po, pi = furnish('li#plexit.list-item', { data: btoa(JSON.stringify(opt)) }, img); + let Db = await Identify(item); - if(po = options.button.querySelector('#plexit')) - po.remove(); - options.button.querySelector('ul').insertBefore(pi, op); - } else { - options.field = 'original_title'; + IMDbID = IMDbID || Db.imdb || 'tt'; + TMDbID = TMDbID || Db.tmdb || 0; + TVDbID = TVDbID || Db.tvdb || 0; - 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' }; + title = title || Db.title; + year = +(year || Db.year || 0); - let po, pi = furnish('li#plexit.list-item', { data: btoa(JSON.stringify(opt)) }, img); + data.splice(index, 1, { type, title, year, image, button, IMDbID, TMDbID, TVDbID }); + } - 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)'; + 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); - modifyPlexButton(options.button, action, title, options); - opt = { ...opt, found: false, status: action }; + let { image, type, title, year, IMDbID, TMDbID, TVDbID } = data; + let Db = await Identify(data); - let po, pi = furnish('li#plexit.list-item', { data: btoa(JSON.stringify(opt)) }, img); + IMDbID = IMDbID || Db.imdb || 'tt'; + TMDbID = TMDbID || Db.tmdb || 0; + TVDbID = TVDbID || Db.tvdb || 0; - 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); - } - }); + 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); } - }) - } 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'); + return true; + + default: + // UTILS_TERMINAL.warn(`Unknown event [${ request.type }]`); + return false; } -} + }); -function getPlexMediaRequest(options) { - if(!(config.plexURL && config.plexToken) || config.DO_NOT_USE) - return new Promise((resolve, reject) => resolve({ found: false, key: null })); + /* Listen for Window events - from iframes, etc. */ + top.addEventListener('message', request => { + try { + request = request.data; - 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) - ); - }); -} + switch(request.type) { + case 'SEND_VIDEO_LINK': + let options = { ...FindMediaItem.OPTIONS, href: request.href, remote: request.from }; -function getPlexMediaURL(PlexUIID, key) { - return config.plexURL.replace(RegExp(`\/(${ config.server.id })?$`), `/web#!/server/` + PlexUIID) + `/details?key=${encodeURIComponent( key )}`; -} + UTILS_TERMINAL.log(`Download Event [${ options.remote }]:`, options); -/* Listen for Plugin events */ -chrome.runtime.onMessage.addListener(async(request, sender) => { - terminal.log(`Plugin event [${ request.plugin }]:`, request); + UpdateButton(MASTER_BUTTON, 'downloader', 'Download', options); + return true; - 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 }`; + case 'NOTIFICATION': + let { state, text, timeout = 7000, callback = () => {}, requiresClick = true } = request.data; + new Notification(state, text, timeout, callback, requiresClick); + return true; - if(!button) - return terminal.warn(BUTTON_ERROR); + default: + // UTILS_TERMINAL.warn(`Unknown event [${ request.type }]`); + return false; + } + } catch(error) { + new Notification('error', `Unable to use downloader: ${ String(error) }`); + throw 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); +})(new Date); - data = data.filter(value => value !== null && value !== undefined); +/* Helpers */ - for(let index = 0, length = data.length, item; index < length; index++) { - let { image, type, title, year, IMDbID, TMDbID, TVDbID } = (item = data[index]); +function wait(on, then) { + if(on && ((on instanceof Function && on()) || true)) + then && then(); + else + setTimeout(() => wait(on, then), 50); +} - if(!item.title || !item.type) - continue; +// the custom "on location change" event +function watchlocationchange(subject) { + let locationchangecallbacks = watchlocationchange.locationchangecallbacks; - let Db = await getIDs(item); + watchlocationchange[subject] = watchlocationchange[subject] || location[subject]; - IMDbID = IMDbID || Db.imdb || 'tt'; - TMDbID = TMDbID || Db.tmdb || 0; - TVDbID = TVDbID || Db.tvdb || 0; + if(watchlocationchange[subject] != location[subject]) { + let from = watchlocationchange[subject], + to = location[subject], + properties = { from, to }, + sign = code => (code + '').replace(/\s+/g, ''); - title = title || Db.title; - year = +(year || Db.year || 0); + watchlocationchange[subject] = location[subject]; - data.splice(index, 1, { type, title, year, image, button, IMDbID, TMDbID, TVDbID }); - } + for(let index = 0, length = locationchangecallbacks.length, callback, called; length > 0 && index < length; index++) { + callback = locationchangecallbacks[index]; + called = locationchangecallbacks.called[sign(callback)]; - if(!data.length) - return terminal.error(PARSING_ERROR); - else - squabblePlex(data, button); - } else { - if(!data.title || !data.type) - return terminal.error(PARSING_ERROR); + let event = new Event('locationchange', { bubbles: true }); - let { image, type, title, year, IMDbID, TMDbID, TVDbID } = data; - let Db = await getIDs(data); + if(!called && callback && typeof callback == 'function') { + locationchangecallbacks.called[sign(callback)] = true; + window.addEventListener('beforeunload', event => { + event.preventDefault(false); - IMDbID = IMDbID || Db.imdb || 'tt'; - TMDbID = TMDbID || Db.tmdb || 0; - TVDbID = TVDbID || Db.tvdb || 0; + callback({ event, ...properties }); + }); - title = title || Db.title; - year = +(year || Db.year || 0); + callback({ event, ...properties }); - findPlexMedia({ type, title, year, image, button, IMDbID, TMDbID, TVDbID }); + open(to, '_self'); + } else { + return /* The eventlistener was already called */; } - 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 }; +} +watchlocationchange.locationchangecallbacks = watchlocationchange.locationchangecallbacks || []; +watchlocationchange.locationchangecallbacks.called = watchlocationchange.locationchangecallbacks.called || {}; - modifyPlexButton(options.button, 'downloader', 'Download', options); - return true; +if(!('onlocationchange' in window)) + Object.defineProperty(window, 'onlocationchange', { + set: callback => (typeof callback == 'function'? watchlocationchange.locationchangecallbacks.push(callback): null), + get: () => watchlocationchange.locationchangecallbacks + }); - default: - // terminal.warn(`Unknown event [${ request.type }]`); - return false; - } - } catch(error) { - new Notification('error', `Unable to use downloader: ${ String(error) }`); - throw error - } -}); +watchlocationchange.onlocationchangeinterval = watchlocationchange.onlocationchangeinterval || setInterval(() => watchlocationchange('href'), 1); +// at least 1s is needed to properly fire the event ._. -String.prototype.toCaps = function toCaps(all) { +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, + 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?) + 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+/); @@ -1704,10 +2134,11 @@ String.prototype.toCaps = function toCaps(all) { 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()); + .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; }; @@ -1732,9 +2163,9 @@ String.prototype.toCaps = function toCaps(all) {
2
3
*/ - parent.queryBy = function queryBy(selectors, container = parent) { + parent.queryBy = parent.queryBy || function queryBy(selectors, container = parent) { // Helpers - let copy = array => [].slice.call(array), + let copy = array => [...array], query = (SELECTORS, CONTAINER = container) => CONTAINER.querySelectorAll(SELECTORS); // Get rid of enclosing syntaxes: [...] and (...) @@ -1765,24 +2196,26 @@ String.prototype.toCaps = function toCaps(all) { 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(/<([^<,]+)?/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, - E = C => [].slice.call(query(ancestor, C)), - F; + let P = element, Q = P.parentElement, R = (Q? Q.parentElement: {}), + E = C => [...query(ancestor, C)], + F, G; - for(let I = 0, L = -generations; ancestor && !!P && I < L; I++) - P = !!~E(P.parentElement).indexOf(P)? P: P.parentElement; + for(let I = 0, L = -generations; ancestor && !!R && !!Q && !!P && I < L; I++) + parent = !!~E(R).indexOf(Q)? Q: G; - parent = ancestor? !~E(P.parentElement).indexOf(P)? null: P: P.parentElement; + for(let I = 0, L = -generations; !!Q && !!P && I < L; I++) + parent = Q = (P = Q).parentElement; - if(!~parents.indexOf(parent)) - parents.push(parent); + if(!~parents.indexOf(parent)) + parents.push(parent); }); media.push(parents.length? parents: elements); } @@ -1811,7 +2244,11 @@ String.prototype.toCaps = function toCaps(all) { child: { value: index => media[index - 1], ...properties - } + }, + empty: { + value: !media.length, + ...properties + }, }); return media; @@ -1820,8 +2257,8 @@ String.prototype.toCaps = function toCaps(all) { /** Adopted from * LICENSE: MIT (2018) */ - parent.furnish = function furnish(name, attributes = {}, ...children) { - let u = v => v && v.length, R = RegExp; + 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 }`); @@ -1842,7 +2279,7 @@ String.prototype.toCaps = function toCaps(all) { 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] || ''); + 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); @@ -1851,9 +2288,23 @@ String.prototype.toCaps = function toCaps(all) { attributes.classList = attributes.classList.join(' '); Object.entries(attributes).forEach( - ([name, value]) => (/^(on|(?:inner|outer)(?:HTML|Text)|textContent|class(?:List|Name)$|value)/.test(name))? + ([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) + element.setAttribute(name, value) ); children