diff --git a/README.md b/README.md index a710d87..a23ec1a 100644 --- a/README.md +++ b/README.md @@ -2,55 +2,66 @@ # Web to Plex ![Icon](src/img/48.png) -![Examples](example.png) +![Examples](https://github.com/SpaceK33z/web-to-plex/blob/master/example.png) This browser extension searches your [Plex Media Server (PMS)](https://www.plex.tv/downloads/) for matching media on sites like [IMDb](https://imdb.com), letting you immediately open the movie or TV show in Plex, if it is available. If the item isn't found on your PMS, then a download button is added instead. ---- -## 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,13 @@ 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 *Notes* diff --git a/src.crx b/src.crx index 455fd7c..16c3343 100644 Binary files a/src.crx and b/src.crx differ diff --git a/src.zip b/src.zip index 65dd4da..a67f55c 100644 Binary files a/src.zip and b/src.zip differ diff --git a/src/background.js b/src/background.js index 4a4b179..8e7eee6 100644 --- a/src/background.js +++ b/src/background.js @@ -62,9 +62,9 @@ function ChangeStatus({ ITEM_ID, ITEM_TITLE, ITEM_TYPE, ID_PROVIDER, ITEM_YEAR, 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_TITLE = ITEM_TITLE.replace(/[\-\s]+/g, '-').replace(/\s*&\s*/g, ' and ').replace(/[^\w\-\'\*\#]+/g, ''), // Search friendly title - SEARCH_PROVIDER = /[it]m/i.test(ID_PROVIDER)? 'GX': 'GG'; + SEARCH_PROVIDER = /^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'); @@ -94,7 +94,7 @@ function ChangeStatus({ ITEM_ID, ITEM_TITLE, ITEM_TYPE, ID_PROVIDER, ITEM_YEAR, }); 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 }); } @@ -342,6 +342,80 @@ function addSonarr(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('|'); + + terminal.group('Generated URL'); + terminal.log('URL', request.url); + terminal.log('Head', headers); + terminal.log('Body', body); + terminal.groupEnd(); + + return debug.body = body; + }) + .then(body => { + return fetch(`${ request.url }`, 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 addOmbi(request, sendResponse) { let headers = { @@ -536,12 +610,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')? @@ -549,7 +623,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; @@ -607,6 +683,9 @@ chrome.runtime.onMessage.addListener((request, sender, callback) => { case 'ADD_SONARR': addSonarr(request, callback); return true; + case 'ADD_MEDUSA': + addMedusa(request, callback); + return true; case 'ADD_WATCHER': addWatcher(request, callback); return true; @@ -647,6 +726,7 @@ chrome.runtime.onMessage.addListener((request, sender, callback) => { case 'PLUGIN': case 'SCRIPT': case '_INIT_': + case '$INIT$': case 'FOUND': /* These are meant to be handled by plugn.js */ return false; diff --git a/src/cloud/__layout__.js b/src/cloud/__layout__.js index 562796f..68fb2d9 100644 --- a/src/cloud/__layout__.js +++ b/src/cloud/__layout__.js @@ -11,7 +11,7 @@ let script = { "ready": () => { /* return a boolean to describe if the page is ready */ }, // optional - "timeout": 1000, // if the script fails to complete, retry after ... milisecoonds + "timeout": 1000, // if the script fails to complete, retry after ... milliseconds // required "init": (ready) => { 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/fandango.js b/src/cloud/fandango.js index 7476ef3..34e6072 100644 --- a/src/cloud/fandango.js +++ b/src/cloud/fandango.js @@ -1,5 +1,5 @@ let script = { - "url": "*://*.fandango.com/movie-overview/*", + "url": "*://*.fandango.com/[\\w\\-]+/movie-overview", "init": (ready) => { let _title, _year, _image, R = RegExp; @@ -11,7 +11,9 @@ let script = { title = title.textContent.trim().split(/\n+/)[0].trim(); year = year.textContent.replace(/.*(\d{4}).*/, '$1').trim(); - image = image.empty? '': image.first.src; + image = image.empty? '': image.src; + + title = title.replace(RegExp(`\\s*\\((${ year })\\)`), ''); return { type, title, year, image }; }, diff --git a/src/cloud/google.play.js b/src/cloud/google.play.js new file mode 100644 index 0000000..431af27 --- /dev/null +++ b/src/cloud/google.play.js @@ -0,0 +1,27 @@ +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' + ), +}; + +addEventListener('popstate', script.init); +addEventListener('pushstate-changed', script.init); diff --git a/src/cloud/gostream.js b/src/cloud/gostream.js index 220e194..03cc7e2 100644 --- a/src/cloud/gostream.js +++ b/src/cloud/gostream.js @@ -11,7 +11,7 @@ let script = { image = $('.hiddenz, [itemprop="image"]').first, type = 'movie'; - new Notification('update', 'Select the Openload (OL) server'); + Notify('update', 'Select the Openload (OL) server'); title = title.textContent.trim(); year = (year? year.textContent.trim(): 0); diff --git a/src/cloud/hulu.js b/src/cloud/hulu.js index 806afeb..9ea56ec 100644 --- a/src/cloud/hulu.js +++ b/src/cloud/hulu.js @@ -1,18 +1,33 @@ let script = { - "url": "*://*.hulu.com/*", + "url": "*://*.hulu.com/(watch|series|movie)/*", - "ready": () => !$('#content [class$="__meta"]').empty, + "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(); - let title = $('#content [class$="__name"]').first, - year = $('#content [class$="__meta"] [class$="segment"]:last-child').first, - image, - type = script.getType(); + title = title.textContent; + } - title = title.textContent.replace(/^\s+|\s+$/g, '').toCaps(); - year = +year.textContent.replace(/.*\((\d{4})\).*/, '$1'); + if(!title) + return 5000; return { type, title, year, image }; }, @@ -20,12 +35,14 @@ let script = { "getType": () => { let { pathname } = top.location; - return pathname.startsWith('/movie/')? - 'movie': - pathname.startsWith('/series/')? - 'show': - 'error'; + if(/^\/series\//.test(pathname)) { + return 'show'; + } else { + let tl = $('[class$="__third-line"]').first; + + return /^\s*$/.test(tl.textContent)? + 'movie': + 'show'; + } }, }; - -window.onlocationchange = script.init; diff --git a/src/cloud/play.google.js b/src/cloud/play.google.js deleted file mode 100644 index 9b3a44f..0000000 --- a/src/cloud/play.google.js +++ /dev/null @@ -1,27 +0,0 @@ -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*(\d{4})\s*\).*?$/, '').trim(); - year = (R.$1 || year.textContent).replace(/^.*?(\d+)/, '$1').trim(); - image = (image || {}).src; - - return { type, title, year, image }; - }, - - "getType": () => ( - location.pathname.startsWith('store/movies/')? - 'movie': - 'show' - ), -}; - -addEventListener('popstate', script.init); -addEventListener('pushstate-changed', script.init); diff --git a/src/cloud/tmdb.js b/src/cloud/tmdb.js index 45f039b..6dd9535 100644 --- a/src/cloud/tmdb.js +++ b/src/cloud/tmdb.js @@ -18,7 +18,7 @@ let script = { image = $('img.poster').first; title = title.textContent.trim(); - year = year.textContent.trim(); + year = +year.textContent.replace(/\(|\)/g, '').trim(); image = (image || {}).src; if(type != 'movie') diff --git a/src/cloud/trakt.js b/src/cloud/trakt.js index 9b6423a..efcfff9 100644 --- a/src/cloud/trakt.js +++ b/src/cloud/trakt.js @@ -93,7 +93,7 @@ let script = { ).first; if(link) - return link.href.replace(/^.*?thetvdb.com\/.+(?:(?:series\/?(?:\?id=)?)(\d+)\b).*?$/, '$1'); + return link.href.replace(/^.*?thetvdb.com\/.+\/(\d+)\b.*?$/, '$1'); }, "process": (element, elements) => { diff --git a/src/cloud/tubi.js b/src/cloud/tubi.js new file mode 100644 index 0000000..765c8ec --- /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 = $('h1').first, + year = $('.Col .Col').first, + image = $('.Col img').first, + type = script.getType(); // described below + + title = title.textContent.trim(); + year = +year.textContent.replace(/[^]*\((\d+)\)[^]*/g, '$1').trim(); + image = image.src; + + return { type, title, year, image }; + }, + + "getType": () => (/^\/movies?/.test(top.location.pathname)? 'movie': 'show'), +}; diff --git a/src/cloud/verizon.js b/src/cloud/verizon.js index dcf8a26..1fe24c2 100644 --- a/src/cloud/verizon.js +++ b/src/cloud/verizon.js @@ -35,8 +35,8 @@ let script = { if(!title) return 1000; - title = title.textContent.trim(); year = +year.textContent.slice(0, 4).trim(); + title = title.textContent.replace(RegExp(`\\s*\\(${ year }\\).*`), '').trim(); image = (image || {}).src; return { type, title, year, image }; 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/youtube.js b/src/cloud/youtube.js index 8def7e6..4a68b8c 100644 --- a/src/cloud/youtube.js +++ b/src/cloud/youtube.js @@ -1,6 +1,11 @@ +let openedByUser = false, + listenersSet = false; + let script = { "url": "*://www.youtube.com/*", + "timeout": 5000, + "init": (ready) => { let _title, _year, _image, R = RegExp; @@ -8,15 +13,15 @@ let script = { close = () => $('.less-button').first.click(), options, type; - if($('.more-button, .less-button').empty) - return 1000; + if($('.more-button, .less-button').empty || !$('.opened').empty) + return script.timeout; open(); // show the year and other information, fails otherwise type = script.getType(); if(type == 'error') - return close(), 1000; + return close(), script.timeout; if(type == 'movie' || type == 'show') { let title = $((type == 'movie'? '.title': '#owner-container')).first, @@ -28,6 +33,8 @@ let script = { 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, @@ -47,6 +54,26 @@ let script = { 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; }, @@ -71,3 +98,5 @@ let script = { top.addEventListener('popstate', script.init); top.addEventListener('pushstate-changed', script.init); + +// $('a[href*="/watch?v="]').forEach(element => element.onclick = event => open(event.target.href, '_self')); diff --git a/src/helpers.js b/src/helpers.js index 1715516..626bffc 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -47,6 +47,10 @@ async function kill(name) { return 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; @@ -54,24 +58,46 @@ function watchlocationchange(subject) { 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; index < length; index++) { + 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 }); - if(callback && typeof callback == 'function') - callback(new Event('locationchange', { bubbles: true })); + 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 => watchlocationchange.locationchangecallbacks.push(callback) + set: callback => (typeof callback == 'function'? watchlocationchange.locationchangecallbacks.push(callback): null), + get: () => watchlocationchange.locationchangecallbacks }); -watchlocationchange.interval = watchlocationchange.interval || setInterval(() => watchlocationchange('href'), 1000); +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) { @@ -84,7 +110,8 @@ String.prototype.toCaps = String.prototype.toCaps || function toCaps(all) { 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?) + 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+/); @@ -100,10 +127,11 @@ String.prototype.toCaps = 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, $$, $_) => $1[0].toUpperCase() + $1.slice(1, $1.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; }; 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/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 f3070f4..ae5b911 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -25,11 +25,18 @@ "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 }, + // Testing purposes only + { + "matches": ["*://ephellon.github.io/web.to.plex/test/*"], + "js": ["utils.js", "sites/__test__.js"], + "css": ["sites/common.css"] + }, + // The sites { "matches": ["*://*.movieo.me/*"], @@ -56,7 +63,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 +115,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"], @@ -135,7 +142,15 @@ "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"] + } ], "background": { @@ -165,5 +180,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 f7c327b..0e77ba1 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 64f4e44..70df308 100644 --- a/src/plugn.js +++ b/src/plugn.js @@ -1,5 +1,5 @@ /* plugn.js (Plugin) - Web to Plex */ -/* global config */ +/* global configuration */ let DISABLE_DEBUGGER = false; @@ -10,6 +10,9 @@ let scribe = let LAST, LAST_JS, LAST_INSTANCE, LAST_ID, LAST_TYPE, FOUND = {}; +let storage = chrome.storage.sync || chrome.storage.local; +let configuration; + function load(name) { return JSON.parse(localStorage.getItem(btoa(name))); } @@ -18,10 +21,154 @@ 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); + } + + 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 /* invalid name */; + + name = 'Cache-Data/' + btoa(name.toLowerCase().replace(/\s+/g, '')); + data = JSON.stringify(data); + + await storage.set({[name]: data}, () => data); + + return name; +} + +function GetConsent(name, builtin) { + return 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); + } + + 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 => (configuration = options), error => { throw error }); } +chrome.storage.onChanged.addListener(async(changes, namespace) => { + await parseConfiguration(); +}); + +(async() => { + await parseConfiguration(); +})(); + function RandomName(length = 16, symbol = '') { let values = []; @@ -32,7 +179,7 @@ function RandomName(length = 16, symbol = '') { let running = [], instance = RandomName(), TAB, cache = {}; -let tabchange = tabs => { +let tabchange = async tabs => { let tab = tabs[0]; if(!tab || FOUND[instance]) return; @@ -42,7 +189,8 @@ let tabchange = tabs => { let id = tab.id, url = tab.url, org, ali, js, - type, cached; + type, cached, + allowed; if( !url @@ -59,12 +207,12 @@ let tabchange = tabs => { url = new URL(url); org = url.origin; ali = url.host.replace(/^(ww\w+\.|\w{2}\.)/i, ''); - can = GetConsent(ali); 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; if(code) { chrome.tabs.executeScript(id, { file: 'helpers.js' }, () => { @@ -90,46 +238,51 @@ let tabchange = tabs => { // Sorry, but the instance needs to be callable multiple times chrome.tabs.executeScript(id, { code: (LAST = cache[ali] = -`/* tabchange */ +`/* ${ type }* (${ (DISABLE_DEBUGGER? 'on':'off') }line) - "${ url.href }" */ ${ name } = (${ name } || (${ name }$ = $ => { - 'use strict'; - - ${ code } - - ;top.onlocationchange = event => ${ type }.init(); - - let InjectedReadyState; - - return (${ type }.RegExp = RegExp( - ${ type }.url - /*.replace(/\\|.*?(\\)|$)/g,'')*/ - .replace(/^\\*\\:/,'\\\\w{3,}:') - .replace(/\\*\\./g,'([^\\\\.]+\\\\.)?') - .replace(/\\/\\*/g,'/[^$]*'),'i') - ).test - ("${ url.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 }' ('${ url.href }') does not match the domain pattern '"+${ type }.url+"' ("+${ type }.RegExp+")"), 5000); +'use strict'; + +if(${ allowed } === false) + return ''; + +/* Start Injected */ +${ code } +/* End Injected */ + +let InjectedReadyState; + +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)) }) @@ -152,14 +305,49 @@ let handle = async(results, tabID, instance, script, type) => { 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 scribe.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 != 'object') + return /* timeout */; + if(typeof data == 'number') return setTimeout(() => { let { request, sender, callback } = (processMessage.properties || {}); processMessage(request, sender, callback) }, data); if(typeof data != 'object') return /* setTimeout */; try { + + 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: 'script', type: 'POPULATE' }); + chrome.tabs.sendMessage(tabID, { data, script, instance, instance_type: type, type: 'POPULATE' }); } catch(error) { throw new Error(InstanceWarning + ' - ' + String(error)); } @@ -175,6 +363,24 @@ chrome.tabs.onActivated.addListener(change => { 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); +}); + +// workaround for the above +chrome.tabs.onActivated.addListener(change => { + instance = RandomName(); + + chrome.tabs.get(change.tabId, tab => tabchange([ tab ])); +}); + chrome.tabs.onUpdated.addListener((ID, change, tab) => { instance = RandomName(); @@ -187,7 +393,7 @@ chrome.tabs.onUpdated.addListener((ID, change, tab) => { // listen for a page load let processMessage; -chrome.runtime.onMessage.addListener(processMessage = (request, sender, callback) => { +chrome.runtime.onMessage.addListener(processMessage = async(request, sender, callback) => { let { options } = request, tab = TAB || {}, { id, url, href } = tab, @@ -215,7 +421,8 @@ chrome.runtime.onMessage.addListener(processMessage = (request, sender, callback if(request && request.options) { let { type } = request, { plugin, script } = options, - _type = type.toLowerCase(); + _type = type.toLowerCase(), + allowed; type = type.toUpperCase(); @@ -227,6 +434,8 @@ chrome.runtime.onMessage.addListener(processMessage = (request, sender, callback switch(type) { case 'PLUGIN': + allowed = await GetConsent(plugin, false); + fetch(file, { mode: 'cors' }) .then(response => response.text()) .then(code => { @@ -234,13 +443,16 @@ chrome.runtime.onMessage.addListener(processMessage = (request, sender, callback // Sorry, but the instance needs to be callable multiple times chrome.tabs.executeScript(id, { code: (LAST = cache[plugin] = -`/* plugin */ +`/* plugin (${ (DISABLE_DEBUGGER? 'on':'off') }line) - "${ url.href }" */ ${ name } = (${ name } || (${ name }$ = $ => { 'use strict'; -${ code } +if(${ allowed } === false) + return ''; -;top.onlocationchange = event => plugin.init(); +/* Start Injected */ +${ code } +/* End Injected */ let PluginReadyState; @@ -251,7 +463,7 @@ return (plugin.RegExp = RegExp( .replace(/\\*\\./g,'([^\\\\.]+\\\\.)?') .replace(/\\/\\*/g,'/[^$]*'),'i') ).test -("${ url.href }")? +(location.href)? /* URL matches pattern */ plugin.ready? /* Plugin has the "ready" property */ @@ -269,11 +481,13 @@ return (plugin.RegExp = RegExp( /* Plugin doesn't have the "ready" property */ plugin.init(): /* URL doesn't match pattern */ -(console.warn("The domain '${ org }' ('${ url.href }') does not match the domain pattern '" + plugin.url + "' (" + plugin.RegExp + ")"), 5000); +(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)) }) @@ -283,6 +497,8 @@ console.log('[${ name }]', ${ name }); break; case 'SCRIPT': + allowed = await GetConsent(script, true); + fetch(file, { mode: 'cors' }) .then(response => response.text()) .then(code => { @@ -290,13 +506,16 @@ console.log('[${ name }]', ${ name }); // Sorry, but the instance needs to be callable multiple times chrome.tabs.executeScript(id, { code: (LAST = cache[script] = -`/* script */ +`/* script (${ (DISABLE_DEBUGGER? 'on':'off') }line) - "${ url.href }" */ ${ name } = (${ name } || (${ name }$ = $ => { 'use strict'; -${ code } +if(${ allowed } === false) + return ''; -;top.onlocationchange = event => script.init(); +/* Start Injected */ +${ code } +/* End Injected */ let ScriptReadyState; @@ -307,7 +526,7 @@ return (script.RegExp = RegExp( .replace(/\\*\\./g,'([^\\\\.]+\\\\.)?') .replace(/\\/\\*/g,'/[^$]*'),'i') ).test -("${ url.href }")? +(location.href)? /* URL matches pattern */ script.ready? /* Script has the "ready" property */ @@ -325,11 +544,13 @@ return (script.RegExp = RegExp( /* Script doesn't have the "ready" property */ script.init(): /* URL doesn't match pattern */ -(console.warn("The domain '${ org }' ('${ url.href }') does not match the domain pattern '" + script.url + "' (" + script.RegExp + ")"), 5000); +(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)) }) @@ -342,6 +563,14 @@ console.log('[${ name }]', ${ name }); 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; diff --git a/src/popup/index.html b/src/popup/index.html index dedd62b..4554a5a 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 { @@ -192,6 +192,14 @@ 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; + } + #local-plex:hover { box-shadow: 0 10px 128px inset #F9BD03; } @@ -216,6 +224,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; @@ -241,7 +253,7 @@ float: right; } - [is-shy] label:after { + [is-shy] label:after, [is-dead] label:after { content: " \1f910"; float: right; } @@ -250,6 +262,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; @@ -301,7 +318,7 @@ - + Verizon @@ -316,13 +333,13 @@ - + Shana Project - + YouTube @@ -337,27 +354,33 @@ + + + Vumoo + + + fandango - + Amazon + + + IMDb - - - CouchPotato @@ -370,36 +393,36 @@ + + + The MovieDb - - - Letterboxd - + Hulu + + + The TVDb - - - Flickmetrix @@ -412,16 +435,16 @@ + + + iTunes - - - - + showRSS @@ -433,21 +456,15 @@ + + + Movieo - - - - - - GoStream - - - TV Maze @@ -482,6 +499,21 @@ + + + + + GoStream + + + + + + Tubi + + + + 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/__test__.js b/src/sites/__test__.js new file mode 100644 index 0000000..a4c3de9 --- /dev/null +++ b/src/sites/__test__.js @@ -0,0 +1,2 @@ +/* global sendUpdate(type:string, details:object) */ +(init = () => sendUpdate('SCRIPT', { script: '__test__' }))(); diff --git a/src/sites/common.css b/src/sites/common.css index 7ea7c9a..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,9 +223,14 @@ max-width: 60% !important; } -.web-to-plex-prompt-option h2 { +.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 { diff --git a/src/sites/flenix/index.js b/src/sites/flenix/index.js deleted file mode 100644 index c5444d6..0000000 --- a/src/sites/flenix/index.js +++ /dev/null @@ -1,50 +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' }); -} \ No newline at end of file diff --git a/src/sites/google/play.js b/src/sites/google/play.js index 448d8a7..6d67eba 100644 --- a/src/sites/google/play.js +++ b/src/sites/google/play.js @@ -1,2 +1,2 @@ /* global sendUpdate(type:string, details:object) */ -(init = () => sendUpdate('SCRIPT', { script: 'play.google' }))(); +(init = () => sendUpdate('SCRIPT', { script: 'google.play' }))(); diff --git a/src/sites/flenix/index.css b/src/sites/tubi/index.css similarity index 100% rename from src/sites/flenix/index.css rename to src/sites/tubi/index.css diff --git a/src/sites/tubi/index.js b/src/sites/tubi/index.js new file mode 100644 index 0000000..f4a7ec8 --- /dev/null +++ b/src/sites/tubi/index.js @@ -0,0 +1,2 @@ +/* global sendUpdate(type:string, details:object) */ +(init = () => sendUpdate('SCRIPT', { script: 'tubi' }))(); 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..d932306 --- /dev/null +++ b/src/sites/vumoo/index.js @@ -0,0 +1,2 @@ +/* global sendUpdate(type:string, details:object) */ +(init = () => sendUpdate('SCRIPT', { script: 'vumoo' }))(); diff --git a/src/utils.js b/src/utils.js index 8995c14..f815fa3 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,1735 +1,1979 @@ /* eslint-disable no-unused-vars */ -/* global config */ - -let DISABLE_DEBUGGER = false; - -let date = (new Date), - terminal = - DISABLE_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(), - NOTIFIED = false; - -let getURL = url => chrome.extension.getURL(url), - $ = (selector, container) => queryBy(selector, container), - init; - -let IMG_URL = { - 'icon_16': getURL('img/16.png'), - 'icon_48': getURL('img/48.png'), - 'icon_white_16': getURL('img/_16.png'), - 'icon_white_48': getURL('img/_48.png'), - 'icon_outline_16': getURL('img/o16.png'), - 'icon_outline_48': getURL('img/o48.png'), - 'hide_icon_16': getURL('img/hide.16.png'), - 'hide_icon_48': getURL('img/hide.48.png'), - 'show_icon_16': getURL('img/show.16.png'), - 'show_icon_48': getURL('img/show.48.png'), - 'plexit_icon_16': getURL('img/plexit.16.png'), - 'plexit_icon_48': getURL('img/plexit.48.png'), - 'reload_icon_16': getURL('img/reload.16.png'), - 'reload_icon_48': getURL('img/reload.48.png'), - 'close_icon_16': getURL('img/close.16.png'), - 'close_icon_48': getURL('img/close.48.png'), - 'settings_icon_16': getURL('img/settings.16.png'), - 'settings_icon_48': getURL('img/settings.48.png'), - 'noise_background': getURL('img/noise.png'), - 'nil': getURL('img/null.png'), -}; +/* global config, init, sendUpdate, "Helpers" */ + +let config, init, sendUpdate; + +(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'), + }; -// the storage -const storage = chrome.storage.sync || chrome.storage.local; + // the storage - priority to sync + const storage = chrome.storage.sync || chrome.storage.local; -async function load(name = '') { - if(!name) return; + async function load(name = '') { + if(!name) + return /* invalid name */; - name = 'Cache-Data/' + btoa(name.toLowerCase().replace(/\s+/g, '')); + 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 new Promise((resolve, reject) => { + function LOAD(DISK) { + let data = JSON.parse(DISK[name] || null); - return resolve(data); - } + return resolve(data); + } - storage.get(null, DISK => { - if (chrome.runtime.lastError) - chrome.storage.local.get(null, LOAD); - else - LOAD(DISK); + 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; - - name = 'Cache-Data/' + btoa(name.toLowerCase().replace(/\s+/g, '')); - data = JSON.stringify(data); - - await storage.set({[name]: data}, () => data); - - return name; -} - -async function kill(name) { - return storage.remove(['Cache-Data/' + btoa(name.toLowerCase().replace(/\s+/g, ''))]); -} - -// create and/or queue a notification -// state = "error" - red -// state = "update" - blue -// state = "info" - grey -// anything else for state will show as orange -class Notification { - constructor(state, text, timeout = 7000, callback = () => {}, requiresClick = true) { - let queue = (Notification.queue = Notification.queue || { list: [] }), - last = queue.list[queue.list.length - 1]; - - if((config.NotifyNewOnly && /\balready (exists|(been )?added)\b/.test(text)) || (config.NotifyOnlyOnce && NOTIFIED && state === 'info')) - return /* Don't match /.../i as to not match item titles */; - - NOTIFIED = true; - - if (last && last.done === false) - 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; + async function save(name = '', data) { + if(!name) + return /* invalid name */; - notification.done = true; - Notification.queue.list.splice(notification.index, 1); - clearTimeout(notification.job); - element.remove(); + name = 'Cache-Data/' + btoa(name.toLowerCase().replace(/\s+/g, '')); + data = JSON.stringify(data); - let removed = delete Notification.queue[notification.id]; + await storage.set({[name]: data}, () => data); - 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]); + return name; + } - document.body.appendChild(element); + async function remove(name) { + if(!name) + return /* invalid name */; - return queue[element.id]; + return await storage.remove(['Cache-Data/' + btoa(name.toLowerCase().replace(/\s+/g, ''))]); } -} -class Prompt { - constructor(prompt_type, options, callback = () => {}, container = document.body) { - let prompt, remove, - array = (options instanceof Array? options: [].slice.call(options)), - data = [...array], - profiles = { - movie: JSON.parse( - config.usingRadarr? - config.radarrQualities: - config.usingWatcher? - config.watcherQualities: - '[]' - ), - show: JSON.parse( - config.usingSonarr? - config.sonarrQualities: - '[]' - ) - }, - locations = { - movie: JSON.parse( - config.usingRadarr? - config.radarrStoragePaths: - config.usingWatcher? - config.watcherStoragePaths: - '[]' - ), - show: JSON.parse( - config.usingSonarr? - config.sonarrStoragePaths: - '[]' - ) - }, - defaults = { - movie: ( - config.usingRadarr? - { quality: config.__radarrQuality, location: config.__radarrStoragePath }: - {} - ), - show: ( - config.usingSonarr? - { quality: config.__sonarrQuality, location: config.__sonarrStoragePath }: - {} - ) + /* 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') && config.NotifyNewOnly && /\balready\s+(exists?|(been\s+)?added)\b/.test(text)) || (config.NotifyOnlyOnce && NOTIFIED && state === 'info')) + return /* Don't match /.../i as to not match item titles */; + NOTIFIED = true; + + if(last && !last.done) + return (last => setTimeout(() => new Notification(state, text, timeout, callback, requiresClick), +(new Date) - last.start))(last); + + let element = furnish(`div.web-to-plex-notification.${state}`, { + onmouseup: event => { + let notification = Notification.queue[event.target.id], + element = notification.element; + + notification.done = true; + Notification.queue.list.splice(notification.index, 1); + clearTimeout(notification.job); + element.remove(); + + let removed = delete Notification.queue[notification.id]; + + return (event.requiresClick)? null: notification.callback(removed); + } + }, text); + + queue[element.id = +(new Date)] = { + start: +element.id, + stop: +element.id + timeout, + span: +timeout, + done: false, + index: queue.list.length, + job: setTimeout(() => element.onmouseup({ target: element, requiresClick }), timeout), + id: +element.id, + callback, element }; + queue.list.push(queue[element.id]); - 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(); + document.body.appendChild(element); - data.splice(+element.value, 1, null); - header.innerText = 'Approve ' + counter.children.length + (counter.children.length == 1?' item': ' items'); + return queue[element.id]; + } + } + + class Prompt { + constructor(prompt_type, options, callback = () => {}, container = document.body) { + let prompt, remove, + array = (options instanceof Array? options: [].slice.call(options)), + data = [...array], + profiles = { + movie: JSON.parse( + config.usingRadarr? + config.radarrQualities: + config.usingWatcher? + config.watcherQualities: + '[]' + ), + show: JSON.parse( + config.usingSonarr? + config.sonarrQualities: + config.usingMedusa? + config.medusaQualities: + '[]' + ) + }, + locations = { + movie: JSON.parse( + config.usingRadarr? + config.radarrStoragePaths: + config.usingWatcher? + config.watcherStoragePaths: + '[]' + ), + show: JSON.parse( + config.usingSonarr? + config.sonarrStoragePaths: + config.usingMedusa? + config.medusaStoragePaths: + '[]' + ) + }, + defaults = { + movie: ( + config.usingRadarr? + { quality: config.__radarrQuality, location: config.__radarrStoragePath }: + {} + ), + show: ( + config.usingSonarr? + { quality: config.__sonarrQuality, location: config.__sonarrStoragePath }: + config.usingMedusa? + { quality: config.__medusaQuality, location: config.__medusaStoragePath }: + {} + ) }; - prompt = furnish('div.web-to-plex-prompt', {}, - furnish('div.web-to-plex-prompt-body', {}, - // The prompt's title - furnish('h1.web-to-plex-prompt-header', {}, 'Approve ' + array.length + (array.length == 1? ' item': ' items')), - - // The prompt's items - furnish('div.web-to-plex-prompt-options', {}, - ...(ITEMS => { - let elements = []; - - for(let index = 0, length = ITEMS.length, ITEM, P_QUA, P_LOC; index < length; index++) { - ITEM = ITEMS[index]; - - elements.push( - furnish('li.web-to-plex-prompt-option.mutable', { value: index, innerHTML: `

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

` }, - furnish('button.remove', { title: `Remove "${ ITEM.title }"`, onmouseup: event => { remove(event.target.parentElement); event.target.remove() } }), - ( - config.PromptQuality? - P_QUA = furnish('select.quality', { index, onchange: event => data[event.target.getAttribute('index')].quality = event.target.value }, ...profiles[/(movie|film|cinema)/i.test(ITEM.type)?'movie':'show'].map(Q => furnish('option', { value: Q.id }, Q.name))): - '' - ),( - config.PromptLocation? - P_LOC = furnish('select.location', { index, onchange: event => data[event.target.getAttribute('index')].location = event.target.value }, ...locations[/(movie|film|cinema)/i.test(ITEM.type)?'movie':'show'].map(Q => furnish('option', { value: Q.id }, Q.path))): - '' + switch(prompt_type) { + /* Allows the user to add and remove items from a list */ + case 'prompt': + case 'input': + remove = element => { + let prompter = $('.web-to-plex-prompt').first, + header = $('.web-to-plex-prompt-header').first, + counter = $('.web-to-plex-prompt-options').first; + + if(element === true) + return prompter.remove(); + else + element.remove(); + + data.splice(+element.value, 1, null); + header.innerText = 'Approve ' + counter.children.length + (counter.children.length == 1?' item': ' items'); + }; + + prompt = furnish('div.web-to-plex-prompt', {}, + furnish('div.web-to-plex-prompt-body', {}, + // The prompt's title + furnish('h1.web-to-plex-prompt-header', {}, 'Approve ' + array.length + (array.length == 1? ' item': ' items')), + + // The prompt's items + furnish('div.web-to-plex-prompt-options', {}, + ...(ITEMS => { + let elements = []; + + for(let index = 0, length = ITEMS.length, ITEM, P_QUA, P_LOC; index < length; index++) { + ITEM = ITEMS[index]; + + elements.push( + furnish('li.web-to-plex-prompt-option.mutable', { value: index, innerHTML: `

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

` }, + furnish('button.remove', { title: `Remove "${ ITEM.title }"`, onmouseup: event => { remove(event.target.parentElement); event.target.remove() } }), + ( + config.PromptQuality? + P_QUA = furnish('select.quality', { index, onchange: event => data[event.target.getAttribute('index')].quality = event.target.value }, ...profiles[/(movie|film|cinema)/i.test(ITEM.type)?'movie':'show'].map(Q => furnish('option', { value: Q.id }, Q.name))): + '' + ),( + config.PromptLocation? + P_LOC = furnish('select.location', { index, onchange: event => data[event.target.getAttribute('index')].location = event.target.value }, ...locations[/(movie|film|cinema)/i.test(ITEM.type)?'movie':'show'].map(Q => furnish('option', { value: Q.id }, Q.path))): + '' + ) ) - ) - ); - - if(P_QUA) P_QUA.value = defaults[ITEM.type].quality; - if(P_LOC) P_LOC.value = defaults[ITEM.type].location; + ); - P_QUA = P_LOC = null; - } + if(P_QUA) P_QUA.value = defaults[ITEM.type].quality; + if(P_LOC) P_LOC.value = defaults[ITEM.type].location; - 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 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; + P_QUA = P_LOC = null; } - 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 }"`); + 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 getIDs({ type, APIID, APIType }); + IMDbID = Db.imdb; + TMDbID = Db.tmdb; + TVDbID = Db.tvdb; + + title = Db.title; + year = Db.year; + } else if(/^([^]+)(\s*\(\d{2,4}\)\s*|\s+\d{2,4}\s+)([\w\s\-]+)$/.test(value)) { + title = R.$1; + year = R.$2 || YEAR + ''; + type = R.$3 || (data.length? data[0].type: 'movie'); + + year = +year.replace(/\D/g, '').replace(/^\d{2}$/, '20$&'); + type = movie.test(type)? 'movie': 'show'; + + Db = await getIDs({ type, title, year }); + IMDbID = Db.imdb; + TMDbID = Db.tmdb; + TVDbID = Db.tvdb; + } + + event.preventDefault(); + if(type && title && !(/^(?:tt)?$/i.test(IMDbID || '') && /^0?$/.test(+TMDbID | 0) && /^0?$/.test(+TVDbID | 0))) { + remove(true); + new Prompt(prompt_type, [{ ...Db, type, IMDbID, TMDbID, TVDbID }, ...data], callback, container); + } else { + self.disabled = self.removeAttribute('disabled'); + self.value = value; + new Notification('error', `Couldn't find "${ value }"`); + } } - } - } }), - 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') + } }), + 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; + ); + break; + + /* Allows the user to remove predetermined items */ + case 'select': + remove = element => { + let prompter = $('.web-to-plex-prompt').first, + header = $('.web-to-plex-prompt-header').first, + counter = $('.web-to-plex-prompt-options').first; + + if(element === true) + return prompter.remove(); + else + element.remove(); + + data.splice(+element.value, 1, null); + header.innerText = 'Approve ' + counter.children.length + (counter.children.length == 1?' item': ' items'); + }; + + prompt = furnish('div.web-to-plex-prompt', {}, + furnish('div.web-to-plex-prompt-body', {}, + // The prompt's title + furnish('h1.web-to-plex-prompt-header', {}, 'Approve ' + array.length + (array.length == 1? ' item': ' items')), + + // The prompt's items + furnish('div.web-to-plex-prompt-options', {}, + ...(ITEMS => { + let elements = []; + + for(let index = 0, length = ITEMS.length, ITEM, P_QUA, P_LOC; index < length; index++) { + ITEM = ITEMS[index]; + + elements.push( + furnish('li.web-to-plex-prompt-option.mutable', { value: index, innerHTML: `

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

` }, + furnish('button.remove', { title: `Remove "${ ITEM.title }"`, onmouseup: event => { remove(event.target.parentElement); event.target.remove() } }), + ( + config.PromptQuality? + P_QUA = furnish('select.quality', { index, onchange: event => data[event.target.getAttribute('index')].quality = event.target.value }, ...profiles[/(movie|film|cinema)/i.test(ITEM.type)?'movie':'show'].map(Q => furnish('option', { value: Q.id }, Q.name))): + '' + ),( + config.PromptLocation? + P_LOC = furnish('select.location', { index, onchange: event => data[event.target.getAttribute('index')].location = event.target.value }, ...locations[/(movie|film|cinema)/i.test(ITEM.type)?'movie':'show'].map(Q => furnish('option', { value: Q.id }, Q.path))): + '' + ) + ) + ); - /* 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(P_QUA) P_QUA.value = defaults[ITEM.type].quality; + if(P_LOC) P_LOC.value = defaults[ITEM.type].location; - 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'); - }; + P_QUA = P_LOC = null; + } - prompt = furnish('div.web-to-plex-prompt', {}, - furnish('div.web-to-plex-prompt-body', {}, - // The prompt's title - furnish('h1.web-to-plex-prompt-header', {}, 'Approve ' + array.length + (array.length == 1? ' item': ' items')), - - // The prompt's items - furnish('div.web-to-plex-prompt-options', {}, - ...(ITEMS => { - let elements = []; - - for(let index = 0, length = ITEMS.length, ITEM, P_QUA, P_LOC; index < length; index++) { - ITEM = ITEMS[index]; - - elements.push( - furnish('li.web-to-plex-prompt-option.mutable', { value: index, innerHTML: `

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

` }, - furnish('button.remove', { title: `Remove "${ ITEM.title }"`, onmouseup: event => { remove(event.target.parentElement); event.target.remove() } }), - ( - config.PromptQuality? - P_QUA = furnish('select.quality', { index, onchange: event => data[event.target.getAttribute('index')].quality = event.target.value }, ...profiles[/(movie|film|cinema)/i.test(ITEM.type)?'movie':'show'].map(Q => furnish('option', { value: Q.id }, Q.name))): - '' - ),( - config.PromptLocation? - P_LOC = furnish('select.location', { index, onchange: event => data[event.target.getAttribute('index')].location = event.target.value }, ...locations[/(movie|film|cinema)/i.test(ITEM.type)?'movie':'show'].map(Q => furnish('option', { value: Q.id }, Q.path))): - '' - ) - ) - ); + return elements + })(array) + ), - if(P_QUA) P_QUA.value = defaults[ITEM.type].quality; - if(P_LOC) P_LOC.value = defaults[ITEM.type].location; + // The engagers + furnish('div.web-to-plex-prompt-footer', {}, + furnish('button.web-to-plex-prompt-decline', { onmouseup: event => { remove(true); callback([]) }, title: 'Close' }, '\u2718'), + furnish('button.web-to-plex-prompt-accept', { onmouseup: event => { remove(true); new Prompt(prompt_type, options, callback, container) }, title: 'Reset' }, '\u21BA'), + furnish('button.web-to-plex-prompt-accept', { onmouseup: event => { remove(true); callback(data.filter(value => value !== null && value !== undefined)) }, title: 'Continue' }, '\u2714') + ) + ) + ); + break; + + /* Allows the user to modify a single item (before being pushed) */ + case 'modify': + let { title, year, type, IMDbID, TMDbID, TVDbID } = options, + P_QUA, P_LOC; + + let i = IMDbID, + t = TMDbID, + v = TVDbID, + s = 'style="text-decoration: none !important; color: #cc7b19 !important; font-style: italic !important;" target="_blank"'; + + i = /^tt-?$/.test(i)? '': i; + t = /^0?$/.test(t)? '': t; + v = /^0?$/.test(v)? '': v; + + remove = element => { + let prompter = $('.web-to-plex-prompt').first, + header = $('.web-to-plex-prompt-header').first, + counter = $('.web-to-plex-prompt-options').first; + + if(element === true) + return prompter.remove(); + else + element.remove(); + }; + + type = /(movie|film|cinema)/i.test(type)?'movie':'show'; + + prompt = furnish('div.web-to-plex-prompt', {}, + furnish('div.web-to-plex-prompt-body', {}, + // The prompt's title + furnish('h1.web-to-plex-prompt-header', { innerHTML: `${ title }${ year? ` (${ year })`: '' } \u2014 ${ type }` }), + + // The prompt's items + furnish('div.web-to-plex-prompt-options', {}, + furnish('div.web-to-plex-prompt-option', { innerHTML: `${ i? `${i}`: '/' } \u2014 ${ t? `${t}`: '/' } \u2014 ${ v? `${v}`: '/' }` }), + ( + config.PromptQuality? + P_QUA = furnish('select.quality', { onchange: event => options.quality = event.target.value }, ...profiles[type].map(Q => furnish('option', { value: Q.id }, Q.name))): + '' + ), + furnish('br'), + ( + config.PromptLocation? + P_LOC = furnish('select.location', { onchange: event => options.location = event.target.value }, ...locations[type].map(Q => furnish('option', { value: Q.id }, Q.path))): + '' + ) + ), + + // The engagers + furnish('div.web-to-plex-prompt-footer', {}, + furnish('button.web-to-plex-prompt-decline', { onmouseup: event => { remove(true); callback([]) }, title: 'Close' }, '\u2718'), + furnish('button.web-to-plex-prompt-accept', { onmouseup: event => { remove(true); new Prompt(prompt_type, options, callback, container) }, title: 'Reset' }, '\u21BA'), + furnish('button.web-to-plex-prompt-accept', { onmouseup: event => { remove(true); callback(options) }, title: 'Continue' }, '\u2714') + ) + ) + ); - P_QUA = P_LOC = null; - } + if(P_QUA) P_QUA.value = defaults[type].quality; + if(P_LOC) P_LOC.value = defaults[type].location; - return elements - })(array) - ), + P_QUA = P_LOC = null; + break; - // 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; + default: + return terminal.warn(`Unknown prompt type "${ prompt_type }"`); + break; + } - default: - return terminal.warn(`Unknown prompt type "${ prompt_type }"`); - break; + return container.append(prompt), prompt; } + } - return container.append(prompt), prompt; + // self explanatory + function openOptionsPage() { + chrome.runtime.sendMessage({ + type: 'OPEN_OPTIONS' + }); } -} -// Send an update query to background.js -function sendUpdate(type, options = {}) { - terminal.log(`Requesting update: ${ type }`, options); + // Send an update query to background.js + sendUpdate = (type, options = {}, postToo) => { + if(config) + terminal.log(`Requesting update: ${ type }`, options); - chrome.runtime.sendMessage({ - type, - options - }); -} + chrome.runtime.sendMessage({ + type, + 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 - }; + if(postToo) + top.postMessage(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; - } + // get the saved options + function __getOptions__() { + return new Promise((resolve, reject) => { + function handleOptions(options) { + if((!options.plexToken || !options.servers) && !options.DO_NOT_USE) + return reject(new Error('Required options are missing')), + null; + + let server, o; + + if(!options.DO_NOT_USE) { + // For now we support only one Plex server, but the options already + // allow multiple for easy migration in the future. + server = options.servers[0]; + o = { + server: { + ...server, + // Compatibility for users who have not updated their settings yet. + connections: server.connections || [{ uri: server.url }] + }, + ...options + }; + + options.plexURL = o.plexURL? + `${ o.plexURL }web#!/server/${ o.server.id }/`: + `https://app.plex.tv/web/app#!/server/${ o.server.id }/`; + } else { + o = options; + } - if (o.couchpotatoBasicAuthUsername) - o.couchpotatoBasicAuth = { - username: o.couchpotatoBasicAuthUsername, - password: o.couchpotatoBasicAuthPassword - }; + 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 + } - // TODO: stupid copy/pasta - if (o.watcherBasicAuthUsername) - o.watcherBasicAuth = { - username: o.watcherBasicAuthUsername, - password: o.watcherBasicAuthPassword - }; + if(o.usingCouchPotato && o.couchpotatoURLRoot && o.couchpotatoToken) { + o.couchpotatoURL = `${ items.couchpotatoURLRoot }/api/${encodeURIComponent(o.couchpotatoToken)}`; + } else { + delete o.couchpotatoURL; // prevent variable ghosting + } - if (o.radarrBasicAuthUsername) - o.radarrBasicAuth = { - username: o.radarrBasicAuthUsername, - password: o.radarrBasicAuthPassword - }; + if(o.usingWatcher && o.watcherURLRoot && o.watcherToken) { + o.watcherURL = o.watcherURLRoot; + } else { + delete o.watcherURL; // prevent variable ghosting + } - if (o.sonarrBasicAuthUsername) - o.sonarrBasicAuth = { - username: o.sonarrBasicAuthUsername, - password: o.sonarrBasicAuthPassword - }; + if(o.usingRadarr && o.radarrURLRoot && o.radarrToken) { + o.radarrURL = o.radarrURLRoot; + } else { + delete o.radarrURL; // prevent variable ghosting + } - if (o.ombiURLRoot && o.ombiToken) { - o.ombiURL = o.ombiURLRoot; - } else { - delete o.ombiURL; // prevent variable ghosting - } + if(o.usingSonarr && o.sonarrURLRoot && o.sonarrToken) { + o.sonarrURL = o.sonarrURLRoot; + } else { + delete o.sonarrURL; // prevent variable ghosting + } - if (o.couchpotatoURLRoot && o.couchpotatoToken) { - o.couchpotatoURL = `${ items.couchpotatoURLRoot }/api/${encodeURIComponent(o.couchpotatoToken)}`; - } else { - delete o.couchpotatoURL; // prevent variable ghosting - } + if(o.usingMedusa && o.medusaURLRoot && o.medusaToken) { + o.medusaURL = o.medusaURLRoot; + } else { + delete o.medusaURL; // prevent variable ghosting + } - if (o.watcherURLRoot && o.watcherToken) { - o.watcherURL = o.watcherURLRoot; - } else { - delete o.watcherURL; // prevent variable ghosting + resolve(o); } - if (o.radarrURLRoot && o.radarrToken) { - o.radarrURL = o.radarrURLRoot; - } else { - delete o.radarrURL; // prevent variable ghosting - } + storage.get(null, options => { + if(chrome.runtime.lastError) + chrome.storage.local.get(null, handleOptions); + else + handleOptions(options); + }); + }); + } - if (o.sonarrURLRoot && o.sonarrToken) { - o.sonarrURL = o.sonarrURLRoot; - } else { - delete o.sonarrURL; // prevent variable ghosting - } + // self explanatory, returns an object; sets the config variable + function parseOptions() { + return __getOptions__() + .then( + options => (config = options), + error => { + new Notification( + 'warning', + 'Fill in missing Web to Plex options', + 15000, + openOptionsPage + ); + throw error; + } + ); + } - resolve(o); - } + await parseOptions(); - storage.get(null, options => { - if (chrome.runtime.lastError) - chrome.storage.local.get(null, handleOptions); - else - handleOptions(options); - }); - }); -} + let AUTO_GRAB = { + ENABLED: config.UseAutoGrab, + LIMIT: config.AutoGrabLimit, + }, + DISABLE_DEBUGGER = !config.ExtensionBranchType, // = { false: Developer Mode, true: Standard Mode } + terminal = + DISABLE_DEBUGGER? + { error: m => m, info: m => m, log: m => m, warn: m => m, group: m => m, groupEnd: m => m }: + console; -// self explanatory -function openOptionsPage() { - chrome.runtime.sendMessage({ - type: 'OPEN_OPTIONS' - }); -} + terminal.log('DISABLE_DEBUGGER:', DISABLE_DEBUGGER, config); -// self explanatory, returns an object -function parseOptions() { - return $getOptions() - .then( - options => (config = options), - error => { - new Notification( - 'warning', - 'Fill in missing Web to Plex options', - 15000, - openOptionsPage - ); - throw error; - } - ); -} + // parse the formatted headers and URL + function HandleProxyHeaders(Headers = "", URL = "") { + let headers = {}; -let config = parseOptions(), - AUTO_GRAB = { - ENABLED: config.UseAutoGrab, - LIMIT: config.AutoGrabLimit, - }; + Headers.replace(/^[ \t]*([^\=\s]+)[ \t]*=[ \t]*((["'`])(?:[^\\\3]*|\\.)\3|[^\f\n\r\v]*)/gm, ($0, $1, $2, $3, $$, $_) => { + let string = !!$3; -function HandleProxyHeaders(Headers = "", URL = "") { - let headers = {}; + if(string) { + headers[$1] = $2.replace(RegExp(`^${ $3 }|${ $3 }$`, 'g'), ''); + } else { + $2 = $2.replace(/@([\w\.]+)/g, (_0, _1, _$, __) => { + let path = _1.split('.'), property = top; - Headers.replace(/^[ \t]*([^\=\s]+)[ \t]*=[ \t]*((["'`])(?:[^\\\3]*|\\.)\3|[^\f\n\r\v]*)/gm, ($0, $1, $2, $3, $$, $_) => { - let string = !!$3; + for(let index = 0, length = path.length; index < length; index++) + property = property[path[index]]; - if(string) { - headers[$1] = $2.replace(RegExp(`^${ $3 }|${ $3 }$`, 'g'), ''); - } else { - $2 = $2.replace(/@([\w\.]+)/g, (_0, _1, _$, __) => { - let path = _1.split('.'), property = top; + headers[$1] = property; + }) + .replace(/@\{b(ase-?)?64-url\}/gi, btoa(URL)) + .replace(/@\{enc(ode)?-url\}/gi, encodeURIComponent(URL)) + .replace(/@\{(raw-)?url\}/gi, URL); + } + }); - for(let index = 0, length = path.length; index < length; index++) - property = property[path[index]]; + return headers; + } - headers[$1] = property; - }) - .replace(/@\{b(ase-?)?64-url\}/gi, btoa(URL)) - .replace(/@\{enc(ode)?-url\}/gi, encodeURIComponent(URL)) - .replace(/@\{(raw-)?url\}/gi, URL); + // 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 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 & 0b1000), // is the user's "Manager Searches" option enabled? + UTF_16 = /[^0\u0020-\u007e, 1\u00a1\u00bf-\u00ff, 2\u0100-\u017f, 3\u0180-\u024f, 4\u0300-\u036f, 5\u0370-\u03ff, 6\u0400-\u04ff, 7\u0500-\u052f, 8\u20a0-\u20bf]+/g; + + type = type || null; + meta = { ...meta, mode: 'cors' }; + rqut = + /(tv|show|series)/i.test(rqut)? + 'tvdb': + /(movie|film|cinema)s?/i.test(rqut)? + 'tmdb': + rqut || '*'; + manable = manable && (config.usingOmbi || (config.usingRadarr && rqut == 'tmdb') || ((config.usingSonarr || config.usingMedusa) && 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 + .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; + `${title} (${year}).${rqut}`.toLowerCase(); + local = await load(savename); } - }); - return headers; -} + if(local) { + terminal.log('[LOCAL] Search results', local); + return local; + } -// 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|cinema)s?/i.test(rqut)? - 'tmdb': - rqut || '*'; - manable = manable && (config.usingOmbi || (config.usingRadarr && rqut == 'tmdb') || (config.usingSonarr && rqut == 'tvdb')); - title = (title? title.replace(/\s*[\:,]\s*seasons?\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); - } + /* the rest of this function is a beautiful mess that will need to be dealt with later... but it works */ + let url = + (manable && title && config.usingOmbi)? + `${ config.ombiURLRoot }api/v1/Search/${ (rqut == 'imdb' || rqut == 'tmdb' || apit == 'movie')? 'movie': 'tv' }/${ plus(title, '%20') }/?apikey=${ api.ombi }`: + (manable && (config.usingRadarr || config.usingSonarr || config.usingMedusa))? + (config.usingRadarr && (rqut == 'imdb' || rqut == 'tmdb'))? + (mid)? + `${ config.radarrURLRoot }api/movie/lookup/tmdb?tmdbId=${ mid }&apikey=${ config.radarrToken }`: + (iid)? + `${ config.radarrURLRoot }api/movie/lookup/imdb?imdbId=${ iid }&apikey=${ config.radarrToken }`: + `${ config.radarrURLRoot }api/movie/lookup?term=${ plus(title, '%20') }&apikey=${ config.radarrToken }`: + (config.usingSonarr)? + (tid)? + `${ config.sonarrURLRoot }api/series/lookup?term=tvdb:${ tid }&apikey=${ config.sonarrToken }`: + `${ config.sonarrURLRoot }api/series/lookup?term=${ plus(title, '%20') }&apikey=${ config.sonarrToken }`: + (config.usingMedusa)? + (tid)? + `${ config.medusarURLRoot }api/v2/series/tvdb${ tid }?detailed=true&${ tid }&api_key=${ config.medusaToken }`: + `${ config.medusaURLRoot }api/v2/internal/searchIndexersForShowName?query=${ plus(title) }&indexerId=0&api_key=${ config.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 = 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 }); + } - if(local) { - terminal.log('[LOCAL] Search results', local); - return local; - } + terminal.log(`Searching for "${ title } (${ year })" in ${ type || apit }/${ rqut }${ proxy.enabled? '[PROXY]': '' } => ${ url }`); - /* the rest of this function is a beautiful mess that will need to be dealt with later... but it works */ - let url = - (manable && title && config.usingOmbi)? - `${ config.ombiURLRoot }api/v1/Search/${ (rqut == 'imdb' || rqut == 'tmdb' || apit == 'movie')? 'movie': 'tv' }/${ plus(title, '%20') }/?apikey=${ api.ombi }`: - (manable && (config.usingRadarr || config.usingSonarr))? - (config.usingRadarr && (rqut == 'imdb' || rqut == 'tmdb'))? - (mid)? - `${ config.radarrURLRoot }api/movie/lookup/tmdb?tmdbId=${ mid }&apikey=${ config.radarrToken }`: - (iid)? - `${ config.radarrURLRoot }api/movie/lookup/imdb?imdbId=${ iid }&apikey=${ config.radarrToken }`: - `${ config.radarrURLRoot }api/movie/lookup?term=${ plus(title, '%20') }&apikey=${ config.radarrToken }`: - (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 }); - } + 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) { + terminal.error(`Failed to parse JSON: "${ data }"`); + } + }) + .catch(error => { + throw error; + }); - terminal.log(`Searching for "${ title } (${ year })" in ${ type || apit }/${ rqut }${ proxy.enabled? '[PROXY]': '' } => ${ url }`); + terminal.log('Search results', { title, year, url, json }); - 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('results' in json) + json = json.results; - terminal.log('Search results', { title, year, url, json }); + 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 = "") => { - if('results' in json) - json = json.results; + 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, '|'] + ]; - 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 = "") => { + for(let i = 0; i < r.length; i++) { + if(/^([\(\|\)]+)?$/.test(s)) return ""; - 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, '|'] - ]; + s = s.replace(r[i][0], r[i][1]); + } - for(let i = 0; i < r.length; i++) { - if(/^([\(\|\)]+)?$/.test(s)) return ""; + return c(s); + }, + R = (s = "", S = "", n = !0) => { + let l = s.split(' ').length, L = S.split(' ').length, + score = 100 * (((S.match(RegExp(`\\b(${k(s)})\\b`, 'i')) || [null]).length) / (L || 1)), + passing = config.UseLooseScore | 0; - s = s.replace(r[i][0], r[i][1]); - } + terminal.log(`=> "${ s }"/"${ S }" = ${ score }`); + score *= (l > L? (L||1)/l: L > l? (l||1)/L: 1); + terminal.log(`~> ... = ${ score }`); - return c(s); - }, - R = (s = "", S = "", n = !0) => { - let score = 100 * ((S.match(RegExp(`\\b(${k(s)})\\b`, 'i')) || [null]).length / (S.split(' ').length || 1)), - passing = config.UseLooseScore | 0; + return (S != '' && score >= passing) || (n? R(S, s, !n): n); + }, + en = /^(u[ks]-?|utf8-?)?en(glish)?$/i; + + // Find an exact match: Title (Year) | #IMDbID + let index, found, $data, lastscore; + for(index = 0, found = false, $data, lastscore = 0; (title && year) && index < json.length && !found; rerun |= 0b0100, index++) { + $data = json[index]; + + let altt = $data.alternativeTitles, + $alt = (altt && altt.length? altt.filter(v => t(v) == t(title))[0]: null); + + // Managers + if(manable) + // Medusa + if(config.usingMedusa && $data instanceof Array) + found = ((t($data[4]) == t(title) || $alt) && +year === +$data[5].slice(0, 4))? + $alt || $data: + found; + // Radarr & Sonarr + else if(config.usingRadarr || config.usingSonarr) + found = ((t($data.title) == t(title) || $alt) && +year === +$data.year)? + $alt || $data: + found; + //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; - 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); - } + terminal.log(`Strict Matching: ${ !!found }`, !!found? found: null); + } - // Find a close match: Title - for(index = 0; title && index < json.length && (!found || lastscore > 0); index++) { - $data = json[index]; - - let altt = $data.alternativeTitles, - $alt = (altt && altt.length? altt.filter(v => c(v) == c(title)): null); - - // Radarr & Sonarr - if(manable) - found = (c($data.title) == c(title) || $alt)? - $alt || $data: - found; - //api.tvmaze.com/ - else if('externals' in ($data = $data.show || $data) || 'show' in $data) - found = - // ignore language barriers - (c($data.name) == c(title))? + // Find a close match: Title + for(index = 0; title && index < json.length && (!found || lastscore > 0); rerun |= 0b0100, index++) { + $data = json[index]; + + let altt = $data.alternativeTitles, + $alt = (altt && altt.length? altt.filter(v => c(v) == c(title)): null); + + // Managers + if(manable) + // Medusa + if(config.usingMedusa && $data instanceof Array) + found = (c($data[4]) == c(title) || $alt)? + $alt || $data: + found; + // Radarr & Sonarr + if(config.usingRadarr || config.usingSonarr) + found = (c($data.title) == c(title) || $alt)? + $alt || $data: + found; + //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: - // trust the api matching - ($data.score > lastscore)? - (lastscore = $data.score || $data.vote_count, $data): found; - //api.themoviedb.org/ \local - else if('movie_results' in $data || 'tv_results' in $data || 'results' in $data) - found = (DATA => { - let i, f, o, l; - - if(DATA.results) - if(rqut == 'tmdb') - DATA.movie_results = DATA.results; - else - DATA.tv_results = DATA.results; - - for(i = 0, f = !1, o = DATA.movie_results, l = o.length | 0; i < l; i++) - f = (c(o.title) == c(title)); - - for(i = (+f * l), o = (f? o: DATA.tv_results), l = (f? l: o.length | 0); i < l; i++) - f = (c(o.name) == c(title)); - - return f? o: f; - })($data); - //api.themoviedb.org/ \remote - else if('original_name' in $data || 'original_title' in $data || 'name' in $data) - found = (c($data.original_name || $data.original_title || $data.name) == c(title))? - $data: - found; - //theimdbapi.org/ - else if(en.test($data.language)) - found = (c($data.title) == c(title))? - $data: - found; - -// terminal.log(`Title Matching: ${ !!found }`, !!found? found: null); - } - // Find an OK match (Loose Searching): Title ~ Title - // The examples below are correct - // GOOD, found: VRV's "Bakemonogatari" vs. TVDb's "Monogatari Series" - // /\b(monogatari)\b/i.test('bakemonogatari') === true - // this is what this option is for - // OK, found: "The Title of This is Bad" vs. "The Title of This is OK" (this is semi-errornous) - // /\b(title|this|bad)\b/i.test('title this ok') === true - // this may be a possible match, but it may also be an error: 'title' and 'this' - // BAD, not found: "Gun Show Showdown" vs. "Gundarr" - // /\b(gun|showdown)\b/i.test('gundarr') === false - // this should not match; the '\b' (border between \w and \W) keeps them from matching - for(index = 0; config.UseLoose && title && index < json.length && (!found || lastscore > 0); index++) { - $data = json[index]; - - let altt = $data.alternativeTitles, - $alt = (altt && altt.length? altt.filter(v => R(v, title)): null); - - // Radarr & Sonarr - if(manable) - found = (R($data.name, title) || $alt)? - $alt || $data: - found; - //api.tvmaze.com/ - else if('externals' in ($data = $data.show || $data) || 'show' in $data) - found = - // ignore language barriers - (R($data.name, title) || terminal.log('Matching:', [$data.name, title], R($data.name, title)))? + terminal.log(`Title Matching: ${ !!found }`, !!found? found: null); + } + + // Find an OK match (Loose Searching): Title ~ Title + // The examples below are correct + // GOOD, found: VRV's "Bakemonogatari" vs. TVDb's "Monogatari Series" + // /\b(monogatari)\b/i.test('bakemonogatari') === true + // this is what this option was designed for + // OK, found: "The Title of This is Bad" vs. "The Title of This is OK" (this is semi-errornous) + // /\b(title|this|bad)\b/i.test('title this ok') === true + // this may be a possible match, but it may also be an error: 'title' and 'this' + // the user's defined threshold is used in this case (above 65% would match these two items) + // BAD, not found: "Gun Show Showdown" vs. "Gundarr" + // /\b(gun|showdown)\b/i.test('gundarr') === false + // this should not match; the '\b' (border between \w and \W) keeps them from matching + for(index = 0; config.UseLoose && title && index < json.length && (!found || lastscore > 0); rerun |= 0b0010, index++) { + $data = json[index]; + + let altt = $data.alternativeTitles, + $alt = (altt && altt.length? altt.filter(v => R(v, title)): null); + + // Managers + if(manable) + // Medusa + if(config.usingMedusa && $data instanceof Array) + found = (R($data[4], title) || $alt)? + $alt || $data: + found; + // Radarr & Sonarr + if(config.usingRadarr || config.usingSonarr) + found = (R($data.name || $data.title, title) || $alt)? + $alt || $data: + found; + //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: - // trust the api matching - ($data.score > lastscore)? - (lastscore = $data.score, $data): found; - //api.themoviedb.org/ \local - else if('movie_results' in $data || 'tv_results' in $data) - found = (DATA => { - let i, f, o, l; - - for(i = 0, f = !1, o = DATA.movie_results, l = o.length | 0; i < l; i++) - f = R(o.title, title); - - for(i = (+f * l), o = (f? o: DATA.tv_results), l = (f? l: o.length | 0); i < l; i++) - f = R(o.name, title); - - return f? o: f; - })($data); - //api.themoviedb.org/ \remote - else if('original_name' in $data || 'original_title' in $data) - found = (R($data.original_name, title) || R($data.original_title, title) || R($data.name, title))? - $data: - found; - //theimdbapi.org/ - else if(en.test($data.language)) - found = (R($data.title, title))? - $data: - found; - -// terminal.log(`Loose Matching: ${ !!found }`, !!found? found: null); - } - json = found; - } + terminal.log(`Loose Matching: ${ !!found }`, !!found? found: null); + } - 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 }; + json = found; + } - let ei = 'tt', - mr = 'movie_results', - tr = 'tv_results'; + if((json === undefined || json === null || json === false) && (rerun & 0b0001)) + return rerun |= 0b0001, json = getIDs({ title, year: YEAR, type, IMDbID, TMDbID, TVDbID, APIType, APIID, meta, rerun }); + else if((json === undefined || json === null)) + json = { IMDbID, TMDbID, TVDbID }; + + let ei = 'tt', + mr = 'movie_results', + tr = 'tv_results'; + + json = json && mr in json? json[mr].length > json[tr].length? json[mr]: json[tr]: json; + + if(json instanceof Array && (!config.usingMedusa? true: (config.usingSonarr || config.usingOmbi))) + json = json[0]; + + if(!json) + json = { IMDbID, TMDbID, TVDbID }; + + // Ombi, Medusa, Radarr and Sonarr + if(manable) + data = ( + (config.usingMedusa && !(config.usingSonarr || config.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 + }; - json = json && mr in json? json[mr].length > json[tr].length? json[mr]: json[tr]: json; + year = +((data.year + '').slice(0, 4)) || 0; + data.year = year; - if(json instanceof Array) - json = json[0]; + let best = { title, year, data, type, rqut, score: json.score | 0 }; - if(!json) - json = { IMDbID, TMDbID, TVDbID }; + terminal.log('Best match:', url, { best, json }); - // 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 - }; + if(best.data.imdb == ei && best.data.tmdb == 0 && best.data.tvdb == 0) + return terminal.log(`No information was found for "${ title } (${ year })"`), {}; - year = +((data.year + '').slice(0, 4)) || 0; - data.year = 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); - let best = { title, year, data, type, rqut, score: json.score | 0 }; + terminal.log(`Saved as "${ savename }"`, data); - terminal.log('Best match:', url, { best, json }); + rerun |= 0b00001; - if(best.data.imdb == ei && best.data.tmdb == 0 && best.data.tvdb == 0) - return terminal.log(`No information was found for "${ title } (${ year })"`), {}; + return data; + } - 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); + 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 already exists in CouchPotato (status: ${response.status})` + ); + } + ); + } - terminal.log(`Saved as "${ savename }"`, data); + // Movies/TV Shows + function pushOmbiRequest(options) { + new Notification('info', `Adding "${ options.title }" to Ombi`, 3000); - return data; -} + if((!options.IMDbID && !options.TMDbID) && !options.TVDbID) { + return new Notification( + 'warning', + 'Stopped adding to Ombi: No content ID' + ); + } -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 already exists in CouchPotato (status: ${response.status})` - ); - } - ); -} + let contentType = (/movies?|film/i.test(options.type)? 'movie': 'tv'); -// Movies/TV Shows -function pushOmbiRequest(options) { - new Notification('info', `Adding "${ options.title }" to Ombi`, 3000); + chrome.runtime.sendMessage({ + type: 'ADD_OMBI', + url: `${ config.ombiURL }api/v1/Request/${ contentType }`, + token: config.ombiToken, + title: options.title, + year: options.year, + imdbId: options.IMDbID, + tmdbId: options.TMDbID, + tvdbId: options.TVDbID, + contentType, + }, + response => { + terminal.log('Pushing to Ombi', response); + + if(response && response.error) { + return new Notification('warning', `Could not add "${ options.title }" to Ombi: ${ response.error }`) || + (!response.silent && terminal.error('Error adding to Ombi: ' + String(response.error), response.location, response.debug)); + } else if(response && response.success) { + let title = options.title.replace(/\&/g, 'and').replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-{2,}/g, '-').toLowerCase(); - if ((!options.IMDbID && !options.TMDbID) && !options.TVDbID) { - return new Notification( - 'warning', - 'Stopped adding to Ombi: No content ID' + terminal.log('Successfully pushed', options); + new Notification('update', `Added "${ options.title }" to Ombi`, 7000, () => window.open(config.ombiURL, '_blank')); + } else { + new Notification('warning', `Could not add "${ options.title }" to Ombi: Unknown Error`) || + (!response.silent && terminal.error('Error adding to Ombi: ' + String(response))); + } + } ); } - 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); + // Movies/TV Shows + function pushCouchPotatoRequest(options) { + new Notification('info', `Adding "${ options.title }" to CouchPotato`, 3000); + + chrome.runtime.sendMessage( + { + type: 'ADD_COUCHPOTATO', + url: `${ config.couchpotatoURL }/movie.add`, + IMDbID: options.IMDbID, + TMDbID: options.TMDbID, + TVDbID: options.TVDbID, + basicAuth: config.couchpotatoBasicAuth, + }, + response => { + terminal.log('Pushing to CouchPotato', response); + + if(response.error) { + return new Notification( + 'warning', + `Could not add "${ options.title }" to CouchPotato (see your console)` + ) || + (!response.silent && terminal.error('Error adding to CouchPotato: ' + String(response.error), response.location, response.debug)); + } + if(response.success) { + terminal.log('Successfully pushed', options); + new Notification('update', `Added "${ options.title }" to CouchPotato`); + } else { + new Notification('warning', `Could not add "${ options.title }" to CouchPotato`); + } + } + ); + } - if (response && response.error) { - return new Notification('warning', `Could not add "${ options.title }" to Ombi: ${ response.error }`) || - (!response.silent && terminal.error('Error adding to Ombi: ' + String(response.error), response.location, response.debug)); - } else if (response && response.success) { - let title = options.title.replace(/\&/g, 'and').replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-{2,}/g, '-').toLowerCase(); + // Movies + function pushWatcherRequest(options) { + new Notification('info', `Adding "${ options.title }" to Watcher`, 3000); - terminal.log('Successfully pushed', options); - new Notification('update', `Added "${ options.title }" to Ombi`, 7000, () => window.open(config.ombiURL, '_blank')); - } else { - new Notification('warning', `Could not add "${ options.title }" to Ombi: Unknown Error`) || - (!response.silent && terminal.error('Error adding to Ombi: ' + String(response))); - } + if(!options.IMDbID && !options.TMDbID) { + return new Notification( + 'warning', + 'Stopped adding to Watcher: No IMDb/TMDb ID' + ); } - ); -} - -// Movies/TV Shows -function pushCouchPotatoRequest(options) { - new Notification('info', `Adding "${ options.title }" to CouchPotato`, 3000); - - chrome.runtime.sendMessage( - { - type: 'ADD_COUCHPOTATO', - url: `${ config.couchpotatoURL }/movie.add`, - IMDbID: options.IMDbID, - TMDbID: options.TMDbID, - TVDbID: options.TVDbID, - basicAuth: config.couchpotatoBasicAuth, - }, - response => { - terminal.log('Pushing to CouchPotato', response); - - if (response.error) { - return new Notification( - 'warning', - `Could not add "${ options.title }" to CouchPotato (see your console)` - ) || - (!response.silent && terminal.error('Error adding to CouchPotato: ' + String(response.error), response.location, response.debug)); - } - if (response.success) { - terminal.log('Successfully pushed', options); - new Notification('update', `Added "${ options.title }" to CouchPotato`); - } else { - new Notification('warning', `Could not add "${ options.title }" to CouchPotato`); - } - } - ); -} -// Movies -function pushWatcherRequest(options) { - new Notification('info', `Adding "${ options.title }" to Watcher`, 3000); - - if (!options.IMDbID && !options.TMDbID) { - return new Notification( - 'warning', - 'Stopped adding to Watcher: No IMDb/TMDb ID' + chrome.runtime.sendMessage({ + type: 'ADD_WATCHER', + url: `${ config.watcherURL }api/`, + token: config.watcherToken, + StoragePath: config.watcherStoragePath, + basicAuth: config.watcherBasicAuth, + title: options.title, + year: options.year, + imdbId: options.IMDbID, + tmdbId: options.TMDbID, + }, + response => { + terminal.log('Pushing to Watcher', response); + + if(response && response.error) { + return new Notification('warning', `Could not add "${ options.title }" to Watcher: ${ response.error }`) || + (!response.silent && terminal.error('Error adding to Watcher: ' + String(response.error), response.location, response.debug)); + } else if(response && (response.success || (response.response + "") == "true")) { + let title = options.title.replace(/\&/g, 'and').replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-{2,}/g, '-').toLowerCase(), + TMDbID = options.TMDbID || response.tmdbId; + + terminal.log('Successfully pushed', options); + new Notification('update', `Added "${ options.title }" to Watcher`, 7000, () => window.open(`${config.watcherURL}library/status${TMDbID? `#${title}-${TMDbID}`: '' }`, '_blank')); + } else { + new Notification('warning', `Could not add "${ options.title }" to Watcher: Unknown Error`) || + (!response.silent && terminal.error('Error adding to Watcher: ' + String(response))); + } + } ); } - chrome.runtime.sendMessage({ - type: 'ADD_WATCHER', - url: `${ config.watcherURL }api/`, - token: config.watcherToken, - StoragePath: config.watcherStoragePath, - basicAuth: config.watcherBasicAuth, - title: options.title, - year: options.year, - imdbId: options.IMDbID, - tmdbId: options.TMDbID, - }, - response => { - terminal.log('Pushing to Watcher', response); - - if (response && response.error) { - return new Notification('warning', `Could not add "${ options.title }" to Watcher: ${ response.error }`) || - (!response.silent && terminal.error('Error adding to Watcher: ' + String(response.error), response.location, response.debug)); - } else if (response && (response.success || (response.response + "") == "true")) { - let title = options.title.replace(/\&/g, 'and').replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-{2,}/g, '-').toLowerCase(), - TMDbID = options.TMDbID || response.tmdbId; - - terminal.log('Successfully pushed', options); - new Notification('update', `Added "${ options.title }" to Watcher`, 7000, () => window.open(`${config.watcherURL}library/status${TMDbID? `#${title}-${TMDbID}`: '' }`, '_blank')); - } else { - new Notification('warning', `Could not add "${ options.title }" to Watcher: Unknown Error`) || - (!response.silent && terminal.error('Error adding to Watcher: ' + String(response))); - } - } - ); -} + // Movies + function pushRadarrRequest(options, prompted) { + if(!options.IMDbID && !options.TMDbID) + return (!prompted)? new Notification( + 'warning', + 'Stopped adding to Radarr: No IMDb/TMDb ID' + ): null; -// Movies -function pushRadarrRequest(options) { - new Notification('info', `Adding "${ options.title }" to Radarr`, 3000); + let PromptValues = {}, + { PromptQuality, PromptLocation } = config; - if (!options.IMDbID && !options.TMDbID) { - return new Notification( - 'warning', - 'Stopped adding to Radarr: No IMDb/TMDb ID' - ); - } + if(!prompted && (PromptQuality || PromptLocation)) + return new Prompt('modify', options, refined => pushRadarrRequest(refined, true)); - let PromptValues = {}, - { PromptQuality, PromptLocation } = config; - - if(PromptQuality && +options.quality > 0) - PromptValues.QualityID = +options.quality; - if(PromptLocation && options.location) - PromptValues.StoragePath = JSON.parse(config.radarrStoragePaths)[+options.location - 1].path.replace(/\\/g, '\\\\'); - - chrome.runtime.sendMessage({ - type: 'ADD_RADARR', - url: `${ config.radarrURL }api/movie/`, - token: config.radarrToken, - StoragePath: config.radarrStoragePath, - QualityID: config.radarrQualityProfileId, - basicAuth: config.radarrBasicAuth, - title: options.title, - year: options.year, - imdbId: options.IMDbID, - tmdbId: options.TMDbID, - ...PromptValues - }, - response => { - terminal.log('Pushing to Radarr', response); - - if (response && response.error) { - return new Notification('warning', `Could not add "${ options.title }" to Radarr: ${ response.error }`) || - (!response.silent && terminal.error('Error adding to Radarr: ' + String(response.error), response.location, response.debug)); - } else if (response && response.success) { - let title = options.title.replace(/\&/g, 'and').replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-{2,}/g, '-').toLowerCase(), - TMDbID = options.TMDbID || response.tmdbId; - - terminal.log('Successfully pushed', options); - new Notification('update', `Added "${ options.title }" to Radarr`, 7000, () => window.open(`${config.radarrURL}${TMDbID? `movies/${title}-${TMDbID}`: '' }`, '_blank')); - } else { - new Notification('warning', `Could not add "${ options.title }" to Radarr: Unknown Error`) || - (!response.silent && terminal.error('Error adding to Radarr: ' + String(response))); - } - } - ); -} + if(PromptQuality && +options.quality > 0) + PromptValues.QualityID = +options.quality; + if(PromptLocation && options.location) + PromptValues.StoragePath = JSON.parse(config.radarrStoragePaths).map(item => item.id == options.location? item: null).filter(n => n)[0].path.replace(/\\/g, '\\\\'); -// TV Shows -function pushSonarrRequest(options) { - new Notification('info', `Adding "${ options.title }" to Sonarr`, 3000); + new Notification('info', `Adding "${ options.title }" to Radarr`, 3000); - if (!options.TVDbID) { - return new Notification( - 'warning', - 'Stopped adding to Sonarr: No TVDb ID' + chrome.runtime.sendMessage({ + type: 'ADD_RADARR', + url: `${ config.radarrURL }api/movie/`, + token: config.radarrToken, + StoragePath: config.radarrStoragePath, + QualityID: config.radarrQualityProfileId, + basicAuth: config.radarrBasicAuth, + title: options.title, + year: options.year, + imdbId: options.IMDbID, + tmdbId: options.TMDbID, + ...PromptValues + }, + response => { + terminal.log('Pushing to Radarr', response); + + if(response && response.error) { + return new Notification('warning', `Could not add "${ options.title }" to Radarr: ${ response.error }`) || + (!response.silent && terminal.error('Error adding to Radarr: ' + String(response.error), response.location, response.debug)); + } else if(response && response.success) { + let title = options.title.replace(/\&/g, 'and').replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-{2,}/g, '-').toLowerCase(), + TMDbID = options.TMDbID || response.tmdbId; + + terminal.log('Successfully pushed', options); + new Notification('update', `Added "${ options.title }" to Radarr`, 7000, () => window.open(`${config.radarrURL}${TMDbID? `movies/${title}-${TMDbID}`: '' }`, '_blank')); + } else { + new Notification('warning', `Could not add "${ options.title }" to Radarr: Unknown Error`) || + (!response.silent && terminal.error('Error adding to Radarr: ' + String(response))); + } + } ); } - let PromptValues = {}, - { PromptQuality, PromptLocation } = config; - - if(PromptQuality && +options.quality > 0) - PromptValues.QualityID = +options.quality; - if(PromptLocation && options.location) - PromptValues.StoragePath = JSON.parse(config.sonarrStoragePaths)[+options.location - 1].path.replace(/\\/g, '\\\\'); - - 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, - ...PromptValues - }, - response => { - terminal.log('Pushing to Sonarr', response); + // TV Shows + function pushSonarrRequest(options, prompted) { + if(!options.TVDbID) + return (!prompted)? new Notification( + 'warning', + 'Stopped adding to Sonarr: No TVDb ID' + ): null; - 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(); + let PromptValues = {}, + { PromptQuality, PromptLocation } = config; - 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))); - } - } - ); -} + if(!prompted && (PromptQuality || PromptLocation)) + return new Prompt('modify', options, refined => pushSonarrRequest(refined, true)); -// make the button -function renderPlexButton(persistent) { - let existingButtons = document.querySelectorAll('.web-to-plex-button'), - firstButton = existingButtons[0]; - - if (existingButtons.length && !persistent) - [].slice.call(existingButtons).forEach(button => button.remove()); - else if(persistent && firstButton !== null && firstButton !== undefined) - return firstButton; - - // + } - document.body.appendChild(button); + // make the button + let MASTER_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; + + // - t = t.join(', '); - t = t.length > 24? t.slice(0, 21).replace(/\W+$/, '') + '...': t; + document.body.appendChild(button); - element.ON_CLICK = e => { - e.preventDefault(); + return MASTER_BUTTON = button; + } - let self = e.target, tv = /tv[\s-]?|shows?|series/i, fail = 0, - options = JSON.parse(atob(button.getAttribute('saved_options'))); + function modifyPlexButton(button, action, title, options = {}) { + let multiple = (action == 'multiple' || options instanceof Array), + element = button.querySelector('.w2p-action, .list-action'), + delimeter = '', + ty = 'Item', txt = 'title', hov = 'tooltip', + em = /^(tt|0)?$/i, + tv = /tv[\s-]?|shows?|series/i; + + if(!element) { + element = button; + button = element.parentElement; + }; - for(let index = 0, length = options.length, option; index < length; index++) { - option = options[index]; + sendUpdate('SEARCH_FOR', { ...options, button }); - try { - if(config.usingOmbi) - pushOmbiRequest(option); - else if (config.usingWatcher && !tv.test(option.type)) - pushWatcherRequest(option); - else if (config.usingRadarr && !tv.test(option.type)) - pushRadarrRequest(option); - else if (config.usingSonarr && tv.test(option.type)) - pushSonarrRequest(option); - else if(config.usingCouchPotato) - $pushAddToCouchpotato(option); - } catch(error) { - terminal.error(`Failed to get "${ option.title }" (Error #${ ++fail })`) - } - } - NOTIFIED = false; + /* Handle a list of items */ + if(multiple) { + options = [].slice.call(options); - if (fail) - new Notification('error', `Failed to grab ${ fail } item${fail==1?'':'s'}`); - }; + let saved_options = [], // a list of successful searches (not on Plex) + len = options.length, + s = (len == 1? '': 's'), + t = []; - 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) })); + for(let index = 0; index < len; index++) { + let option = options[index]; - 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 */ + // Skip empty entries + if(!option || !option.type || !option.title) continue; - if(!options || !options.type || !options.title) return; + // 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 }`; - let empty = (em.test(options.IMDbID) && em.test(options.TMDbID) && em.test(options.TVDbID)), - nice_title = `${options.title.toCaps()}${options.year? ` (${options.year})`: ''}`; + /* Failed */ + if(/#tt-0-0/i.test(url)) + continue; - if(options) { - ty = (options.type == 'movie'? 'Movie': 'TV Show'); - txt = options.txt || txt; - hov = options.hov || hov; - } + saved_options.push(option); + t.push(option.title); + } - 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.usingOmbi) { - path = ''; - } else if (config.usingWatcher && !tv.test(options.type)) { - path = ''; - } else if (config.usingRadarr && !tv.test(options.type)) { - path = config.radarrStoragePath; - } else if (config.usingSonarr && tv.test(options.type)) { - path = config.sonarrStoragePath; - } else if(config.usingCouchPotato) { - path = ''; + t = t.join(', '); + t = t.length > 24? t.slice(0, 21).replace(/\W+$/, '') + '...': t; + + element.ON_CLICK = e => { + e.preventDefault(); + + let self = e.target, tv = /tv[\s-]?|shows?|series/i, fail = 0, + options = JSON.parse(atob(button.getAttribute('saved_options'))); + + for(let index = 0, length = options.length, option; index < length; index++) { + option = options[index]; + + try { + if(config.usingOmbi) + pushOmbiRequest(option); + else if(config.usingWatcher && !tv.test(option.type)) + pushWatcherRequest(option); + else if(config.usingRadarr && !tv.test(option.type)) + pushRadarrRequest(option); + else if(config.usingSonarr && tv.test(option.type)) + pushSonarrRequest(option); + else if(config.usingMedusa && tv.test(option.type)) + pushMedusaRequest(option); + else if(config.usingCouchPotato) + $pushAddToCouchpotato(option); + } catch(error) { + terminal.error(`Failed to get "${ option.title }" (Error #${ ++fail })`) } + } + NOTIFIED = false; - 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(); + if(fail) + new Notification('error', `Failed to grab ${ fail } item${fail==1?'':'s'}`); + }; - sendUpdate('DOWNLOAD_FILE', { ...options, button, href, path }); - new Notification('update', 'Opening prompt (may take a while)...'); - }); + button.setAttribute('saved_options', btoa(JSON.stringify(saved_options))); + element.addEventListener('click', e => (AUTO_GRAB.ENABLED && AUTO_GRAB.LIMIT > options.length)? element.ON_CLICK(e): new Prompt('select', options, o => { button.setAttribute('saved_options', btoa(JSON.stringify(o))); element.ON_CLICK(e) })); - element.setAttribute(hov, `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; + element.setAttribute(hov, `Grab ${len} new item${s}: ${ t }`); + button.classList.add(saved_options.length || len? 'wtp--download': 'wtp--error'); + } else { + /* Handle a single item */ + if(!options || !options.type || !options.title) return; - /* 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.usingOmbi) { - pushOmbiRequest(options); - } else if (config.usingWatcher && !tv.test(options.type)) { - pushWatcherRequest(options); - } else if (config.usingRadarr && !tv.test(options.type)) { - pushRadarrRequest(options); - } else if (config.usingSonarr && tv.test(options.type)) { - pushSonarrRequest(options); + let empty = (em.test(options.IMDbID) && em.test(options.TMDbID) && em.test(options.TVDbID)), + nice_title = `${options.title.toCaps()}${options.year? ` (${options.year})`: ''}`; + + if(options) { + ty = (options.type == 'movie'? 'Movie': 'TV Show'); + txt = options.txt || txt; + hov = options.hov || hov; + } + + if(action == 'found') { + element.href = getPlexMediaURL(config.server.id, options.key); + element.setAttribute(hov, `Watch "${options.title} (${options.year})" on Plex`); + button.classList.add('wtp--found'); + + new Notification('success', `Watch "${ nice_title }"`, 7000, e => element.click(e)); + } else if(action == 'downloader' || options.remote) { + + switch(options.remote) { + /* Vumoo */ + case 'oload': + let href = options.href, path = ''; + + if(config.usingOmbi) { + path = ''; + } else if(config.usingWatcher && !tv.test(options.type)) { + path = ''; + } else if(config.usingRadarr && !tv.test(options.type)) { + path = config.radarrStoragePath; + } else if(config.usingSonarr && tv.test(options.type)) { + path = config.sonarrStoragePath; + } else if(config.usingMedusa && tv.test(options.type)) { + path = config.medusaStoragePath; } else if(config.usingCouchPotato) { - $pushAddToCouchpotato(options); + path = ''; } - }); - } - NOTIFIED = false; - element.setAttribute(hov, `Add "${ nice_title }" | ${ty}`); - element.style.removeProperty('display'); - } else if (action == 'notfound' || action == 'error' || empty) { - element.removeAttribute('href'); + 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(); - empty = !(options && options.title); + sendUpdate('DOWNLOAD_FILE', { ...options, button, href, path }); + new Notification('update', 'Opening prompt (may take a while)...'); + }); - if(empty) - element.setAttribute(hov, `${ty || 'Item'} not found`); - else - element.setAttribute(hov, `"${ nice_title }" was not found`); + element.setAttribute(hov, `Download "${ nice_title }" | ${ty}`); + sendUpdate('SAVE_AS', { ...options, button, href, path }); + new Notification('update', `"${ nice_title }" can be downloaded`, 7000, e => element.click(e)); + return; + + + /* Default & Error */ + default: + let url = `#${ options.IMDbID || 'tt' }-${ options.TMDbID | 0 }-${ options.TVDbID | 0 }`; + + /* Failed */ + if(/#tt-0-0/i.test(url)) + return modifyPlexButton(button, 'notfound', title, options); + + element.href = url; + button.classList.add('wtp--download'); + element.addEventListener('click', element.ON_CLICK = e => { + e.preventDefault(); + if(config.usingOmbi) { + pushOmbiRequest(options); + } else if(config.usingWatcher && !tv.test(options.type)) { + pushWatcherRequest(options); + } else if(config.usingRadarr && !tv.test(options.type)) { + pushRadarrRequest(options); + } else if(config.usingSonarr && tv.test(options.type)) { + pushSonarrRequest(options); + } else if(config.usingMedusa && tv.test(options.type)) { + pushMedusaRequest(options); + } else if(config.usingCouchPotato) { + $pushAddToCouchpotato(options); + } + }); + } + NOTIFIED = false; - button.classList.remove('wtp--found'); - button.classList.add('wtp--error'); - } + element.setAttribute(hov, `Add "${ nice_title }" | ${ty}`); + element.style.removeProperty('display'); + } else if(action == 'notfound' || action == 'error' || empty) { + element.removeAttribute('href'); + + empty = !(options && options.title); + + if(empty) + element.setAttribute(hov, `${ty || 'Item'} not found`); + else + element.setAttribute(hov, `"${ nice_title }" was not found`); - element.id = options? `${options.IMDbID || 'tt'}-${options.TMDbID | 0}-${options.TVDbID | 0}`: 'tt-0-0'; + button.classList.remove('wtp--found'); + button.classList.add('wtp--error'); + } + + element.id = options? `${options.IMDbID || 'tt'}-${options.TMDbID | 0}-${options.TVDbID | 0}`: 'tt-0-0'; + } } -} -async function squabblePlexMedia(options, button) { - if(!(options && options.length && button)) - return; + async function squabblePlexMedia(options, button) { + if(!(options && options.length && button)) + return; - let results = [], - length = options.length, - queries = (squabblePlexMedia.queries = squabblePlexMedia.queries || {}); + let results = [], + length = options.length, + queries = (squabblePlexMedia.queries = squabblePlexMedia.queries || {}); - squabblePlexMedia.OPTIONS = options; + squabblePlexMedia.OPTIONS = options; - let query = JSON.stringify(options); + let query = JSON.stringify(options); - query = (queries[query] = queries[query] || {}); + query = (queries[query] = queries[query] || {}); - if(query.running === true) - return; - else if(query.results) { - let { results, multiple, items } = query; + if(query.running === true) + return; + else if(query.results) { + let { results, multiple, items } = query; - new Notification('update', `Welcome back. ${ multiple } new ${ items } can be grabbed`, 7000, (event, target = button.querySelector('.list-action')) => target.click({ ...event, target })); + new Notification('update', `Welcome back. ${ multiple } new ${ items } can be grabbed`, 7000, (event, target = button.querySelector('.list-action')) => target.click({ ...event, target })); - if (multiple) - modifyPlexButton(button, 'multiple', `Download ${ multiple } ${ items }`, results); + if(multiple) + modifyPlexButton(button, 'multiple', `Download ${ multiple } ${ items }`, results); - return; - } + return; + } - query.running = true; + query.running = true; - new Notification('info', `Processing ${ length } item${ 's'[+(length === 1)] || '' }...`); + new Notification('info', `Processing ${ length } item${ 's'[+(length === 1)] || '' }...`); - for(let index = 0, option, opt; index < length; index++) { - let { IMDbID, TMDbID, TVDbID } = (option = await options[index]); + for(let index = 0, option, opt; index < length; index++) { + let { IMDbID, TMDbID, TVDbID } = (option = await options[index]); - opt = { name: option.title, title: option.title, year: option.year, image: options.image, type: option.type, imdb: IMDbID, IMDbID, tmdb: TMDbID, TMDbID, tvdb: TVDbID, TVDbID }; + opt = { name: option.title, title: option.title, year: option.year, image: options.image, type: option.type, imdb: IMDbID, IMDbID, tmdb: TMDbID, TMDbID, tvdb: TVDbID, TVDbID }; - try { - await getPlexMediaRequest(option) - .then(async({ found, key }) => { - if (found) { - // ignore found items, we only want new items - } else { - option.field = 'original_title'; + try { + await getPlexMediaRequest(option) + .then(async({ found, key }) => { + if(found) { + // ignore found items, we only want new items + } else { + option.field = 'original_title'; - return await getPlexMediaRequest(option) - .then(({ found, key }) => { - if (found) { - // ignore found items, we only want new items - } else { - let available = (config.usingOmbi || config.usingWatcher || config.usingRadarr || config.usingSonarr || config.usingCouchPotato), - action = (available ? 'downloader' : 'notfound'), - title = available ? - 'Not on Plex (download available)': - 'Not on Plex (download not available)'; + return await getPlexMediaRequest(option) + .then(({ found, key }) => { + if(found) { + // ignore found items, we only want new items + } else { + let available = (config.usingOmbi || config.usingWatcher || config.usingRadarr || config.usingSonarr || config.usingMedusa || config.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) { - terminal.error('Request to Plex failed: ' + String(error)); - // new Notification('error', 'Failed to query item #' + (index + 1)); + results.push({ ...opt, found: false, status: action }); + } + }); + } + }) + } catch(error) { + terminal.error('Request to Plex failed: ' + String(error)); + // new Notification('error', 'Failed to query item #' + (index + 1)); + } } - } - results = results.filter(v => v.status == 'downloader'); + results = results.filter(v => v.status == 'downloader'); - let img = furnish('img', { title: 'Add to Plex It!', src: IMG_URL.plexit_icon_48, onmouseup: event => {let frame = document.querySelector('#plexit-bookmarklet-frame'); frame.src = frame.src.replace(/(#plexit:.*)?$/, '#plexit:' + event.target.parentElement.getAttribute('data'))} }), - po, pi = furnish('li#plexit.list-item', { data: btoa(JSON.stringify(results)) }, img), - op = document.querySelector('#wtp-plexit'); + 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(po = button.querySelector('#plexit')) - po.remove(); - try { - button.querySelector('ul').insertBefore(pi, op); - } catch(e) { /* Don't do anything */ } + if(po = button.querySelector('#plexit')) + po.remove(); + try { + button.querySelector('ul').insertBefore(pi, op); + } catch(e) { /* Don't do anything */ } - let multiple = results.length, - items = multiple == 1? 'item': 'items'; + let multiple = results.length, + items = multiple == 1? 'item': 'items'; - new Notification('update', `Done. ${ multiple } new ${ items } can be grabbed`, 7000, (event, target = button.querySelector('.list-action')) => target.click({ ...event, target })); + new Notification('update', `Done. ${ multiple } new ${ items } can be grabbed`, 7000, (event, target = button.querySelector('.list-action')) => target.click({ ...event, target })); - query.running = false; - query.results = results; - query.multiple = multiple; - query.items = items; + query.running = false; + query.results = results; + query.multiple = multiple; + query.items = items; - if (multiple) - modifyPlexButton(button, 'multiple', `Download ${ multiple } ${ items }`, results); -} + if(multiple) + modifyPlexButton(button, 'multiple', `Download ${ multiple } ${ items }`, results); + } -function findPlexMedia(options) { - if(!(options && options.title)) - return; + function findPlexMedia(options) { + if(!(options && options.title)) + return; - let { IMDbID, TMDbID, TVDbID } = options; + let { IMDbID, TMDbID, TVDbID } = options; - TMDbID = +TMDbID; - TVDbID = +TVDbID; + TMDbID = +TMDbID; + TVDbID = +TVDbID; - let opt = { name: options.title, year: options.year, image: options.image || IMG_URL.nil, type: options.type, imdb: IMDbID, IMDbID, tmdb: TMDbID, TMDbID, tvdb: TVDbID, TVDbID }, - op = document.querySelector('#wtp-plexit'), - img = (options.image)? - furnish('div', { tooltip: 'Add to Plex It!', style: `background: url(${ IMG_URL.plexit_icon_16 }) top right/60% no-repeat, #0004 url(${ opt.image }) center/contain no-repeat; height: 48px; width: 34px;`, draggable: true, onmouseup: event => {let frame = document.querySelector('#plexit-bookmarklet-frame'); frame.src = frame.src.replace(/(#plexit:.*)?$/, '#plexit:' + event.target.parentElement.getAttribute('data'))} }): - furnish('img', { title: 'Add to Plex It!', src: IMG_URL.plexit_icon_48, onmouseup: event => {let frame = document.querySelector('#plexit-bookmarklet-frame'); frame.src = frame.src.replace(/(#plexit:.*)?$/, '#plexit:' + event.target.parentElement.getAttribute('data'))} }); + 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'))} }); - findPlexMedia.OPTIONS = options; + findPlexMedia.OPTIONS = options; - try { - 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' }; + try { + return getPlexMediaRequest(options) + .then(({ found, key }) => { + if(found) { + modifyPlexButton(options.button, 'found', 'On Plex', { ...options, key }); + opt = { ...opt, url: options.button.href, found: true, status: 'found' }; + + let po, pi = furnish('li#plexit.list-item', { data: btoa(JSON.stringify(opt)) }, img); + + if(po = options.button.querySelector('#plexit')) + po.remove(); + try { + options.button.querySelector('ul').insertBefore(pi, op); + } catch(e) { /* Don't do anything */ } + } else { + options.field = 'original_title'; - let po, pi = furnish('li#plexit.list-item', { data: btoa(JSON.stringify(opt)) }, img); + 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' }; - if(po = options.button.querySelector('#plexit')) - po.remove(); - try { - options.button.querySelector('ul').insertBefore(pi, op); - } catch(e) { /* Don't do anything */ } - } else { - options.field = 'original_title'; + let po, pi = furnish('li#plexit.list-item', { data: btoa(JSON.stringify(opt)) }, img); - 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' }; + if(po = options.button.querySelector('#plexit')) + po.remove(); + try { + options.button.querySelector('ul').insertBefore(pi, op); + } catch(e) { /* Don't do anything */ } + } else { + let available = (config.usingOmbi || config.usingWatcher || config.usingRadarr || config.usingSonarr || config.usingMedusa || config.usingCouchPotato), + action = (available ? 'downloader' : 'notfound'), + title = available ? + 'Not on Plex (download available)': + 'Not on Plex (download not available)'; - let po, pi = furnish('li#plexit.list-item', { data: btoa(JSON.stringify(opt)) }, img); + modifyPlexButton(options.button, action, title, options); + opt = { ...opt, found: false, status: action }; - if(po = options.button.querySelector('#plexit')) - po.remove(); - try { - options.button.querySelector('ul').insertBefore(pi, op); - } catch(e) { /* Don't do anything */ } - } else { - let available = (config.usingOmbi || config.usingWatcher || config.usingRadarr || config.usingSonarr || config.usingCouchPotato), - action = (available ? 'downloader' : 'notfound'), - title = available ? - 'Not on Plex (download available)': - 'Not on Plex (download not available)'; + let po, pi = furnish('li#plexit.list-item', { data: btoa(JSON.stringify(opt)) }, img); - modifyPlexButton(options.button, action, title, options); - opt = { ...opt, found: false, status: action }; + if(po = options.button.querySelector('#plexit')) + po.remove(); + if(!!~[].slice.call(options.button.querySelector('ul').children).indexOf(op)) + try { + options.button.querySelector('ul').insertBefore(pi, op); + } catch(e) { /* Don't do anything */ } + } + return found; + }); + } + return found; + }) + } catch(error) { + return modifyPlexButton( + options.button, + 'error', + 'Request to Plex Media Server failed', + options + ), + terminal.error(`Request to Plex failed: ${ String(error) }`), + false; + // new Notification('Failed to communicate with Plex'); + } + } - let po, pi = furnish('li#plexit.list-item', { data: btoa(JSON.stringify(opt)) }, img); + function getPlexMediaRequest(options) { + if(!(config.plexURL && config.plexToken) || config.DO_NOT_USE) + return new Promise((resolve, reject) => resolve({ found: false, key: null })); - if(po = options.button.querySelector('#plexit')) - po.remove(); - if(!!~[].slice.call(options.button.querySelector('ul').children).indexOf(op)) - try { - options.button.querySelector('ul').insertBefore(pi, op); - } catch(e) { /* Don't do anything */ } - } - return found; - }); - } - return found; - }) - } catch(error) { - return modifyPlexButton( - options.button, - 'error', - 'Request to Plex Media Server failed', - options - ), - terminal.error(`Request to Plex failed: ${ String(error) }`), - false; - // new Notification('Failed to communicate with Plex'); - } -} + 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) + ); + }); + } -function getPlexMediaRequest(options) { - if(!(config.plexURL && config.plexToken) || config.DO_NOT_USE) - return new Promise((resolve, reject) => resolve({ found: false, key: null })); + function getPlexMediaURL(PlexUIID, key) { + return config.plexURL.replace(RegExp(`\/(${ config.server.id })?$`), `/web#!/server/` + PlexUIID) + `/details?key=${encodeURIComponent( key )}`; + } - 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) - ); - }); -} + /* Listen for events */ + chrome.runtime.onMessage.addListener(async(request, sender) => { + terminal.log(`Listener event [${ request.instance_type }#${ request[request.instance_type.toLowerCase()] }]:`, request); -function getPlexMediaURL(PlexUIID, key) { - return config.plexURL.replace(RegExp(`\/(${ config.server.id })?$`), `/web#!/server/` + PlexUIID) + `/details?key=${encodeURIComponent( key )}`; -} + 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 }`; -/* Listen for events */ -chrome.runtime.onMessage.addListener(async(request, sender) => { - terminal.log(`Listener event [${ request.instance_type }#${ request[request.instance_type] }]:`, request); + if(!data) + return terminal.warn(EMPTY_REQUEST); + let button = renderPlexButton(); - 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(!button) + return terminal.warn(BUTTON_ERROR); - if(!data) - return terminal.warn(EMPTY_REQUEST); - let button = renderPlexButton(); + switch(request.type) { + case 'POPULATE': + + if(data instanceof Array) { + for(let index = 0, length = data.length, item; index < length; index++) + if(!(item = data[index]) || !item.type) + data.splice(index, 1, null); - if(!button) - return terminal.warn(BUTTON_ERROR); + data = data.filter(value => value !== null && value !== undefined); - switch(request.type) { - case 'POPULATE': + for(let index = 0, length = data.length, item; index < length; index++) { + let { image, type, title, year, IMDbID, TMDbID, TVDbID } = (item = data[index]); - 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); + if(!item.title || !item.type) + continue; - data = data.filter(value => value !== null && value !== undefined); + let Db = await getIDs(item); - for(let index = 0, length = data.length, item; index < length; index++) { - let { image, type, title, year, IMDbID, TMDbID, TVDbID } = (item = data[index]); + IMDbID = IMDbID || Db.imdb || 'tt'; + TMDbID = TMDbID || Db.tmdb || 0; + TVDbID = TVDbID || Db.tvdb || 0; - if(!item.title || !item.type) - continue; + title = title || Db.title; + year = +(year || Db.year || 0); - let Db = await getIDs(item); + data.splice(index, 1, { type, title, year, image, button, IMDbID, TMDbID, TVDbID }); + } + + if(!data.length) + return terminal.error(PARSING_ERROR); + else + squabblePlexMedia(data, button); + } else { + if(!data || !data.title || !data.type) + return terminal.error(PARSING_ERROR); + + let { image, type, title, year, IMDbID, TMDbID, TVDbID } = data; + let Db = await getIDs(data); IMDbID = IMDbID || Db.imdb || 'tt'; TMDbID = TMDbID || Db.tmdb || 0; @@ -1738,91 +1982,104 @@ chrome.runtime.onMessage.addListener(async(request, sender) => { title = title || Db.title; year = +(year || Db.year || 0); - data.splice(index, 1, { type, title, year, image, button, IMDbID, TMDbID, TVDbID }); + let found = await findPlexMedia({ type, title, year, image, button, IMDbID, TMDbID, TVDbID }); + sendUpdate('FOUND', { ...request, found }, true); } + return true; - if(!data.length) - return terminal.error(PARSING_ERROR); - else - squabblePlexMedia(data, button); - } else { - if(!data || !data.title || !data.type) - return terminal.error(PARSING_ERROR); - - let { image, type, title, year, IMDbID, TMDbID, TVDbID } = data; - let Db = await getIDs(data); + default: + // terminal.warn(`Unknown event [${ request.type }]`); + return false; + } + }); - IMDbID = IMDbID || Db.imdb || 'tt'; - TMDbID = TMDbID || Db.tmdb || 0; - TVDbID = TVDbID || Db.tvdb || 0; + /* Listen for Window events - from iframes, etc. */ + top.addEventListener('message', request => { + try { + request = request.data; - title = title || Db.title; - year = +(year || Db.year || 0); + switch(request.type) { + case 'SEND_VIDEO_LINK': + let options = { ...findPlexMedia.OPTIONS, href: request.href, remote: request.from }; - let found = await findPlexMedia({ type, title, year, image, button, IMDbID, TMDbID, TVDbID }); - sendUpdate('FOUND', { ...request, found }); - } - return true; + terminal.log('oload Event:', options); - default: -// terminal.warn(`Unknown event [${ request.type }]`); - return false; - } -}); + modifyPlexButton(MASTER_BUTTON, 'downloader', 'Download', options); + return true; -/* Listen for Window events - from iframes, etc. */ -top.addEventListener('message', request => { - try { - request = request.data; + case 'NOTIFICATION': + let { state, text, timeout = 7000, callback = () => {}, requiresClick = true } = request.data; + new Notification(state, text, timeout, callback, requiresClick); + return true; - switch(request.type) { - case 'SEND_VIDEO_LINK': - let options = { ...findPlexMedia.OPTIONS, href: request.href, remote: request.from }; + default: + // terminal.warn(`Unknown event [${ request.type }]`); + return false; + } + } catch(error) { + new Notification('error', `Unable to use downloader: ${ String(error) }`); + throw error + } + }); - modifyPlexButton(options.button, 'downloader', 'Download', options); - return true; +})(new Date); - default: - // terminal.warn(`Unknown event [${ request.type }]`); - return false; - } - } catch(error) { - new Notification('error', `Unable to use downloader: ${ String(error) }`); - throw error - } -}); +/* Helpers */ function wait(on, then) { - if (on && on()) + if(on && ((on instanceof Function && on()) || true)) then && then(); else setTimeout(() => wait(on, then), 50); } // the custom "on location change" event -let locationchangecallbacks = []; - function watchlocationchange(subject) { + let locationchangecallbacks = watchlocationchange.locationchangecallbacks; + watchlocationchange[subject] = watchlocationchange[subject] || location[subject]; - if (watchlocationchange[subject] != location[subject]) { + if(watchlocationchange[subject] != location[subject]) { + let from = watchlocationchange[subject], + to = location[subject], + properties = { from, to }, + sign = code => (code + '').replace(/\s+/g, ''); + watchlocationchange[subject] = location[subject]; - for(let index = 0, length = locationchangecallbacks.length, callback; index < length; index++) { + 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 }); + }); - if(callback && typeof callback == 'function') - callback(new Event('locationchange', { bubbles: true })); + 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 => locationchangecallbacks.push(callback) + set: callback => (typeof callback == 'function'? watchlocationchange.locationchangecallbacks.push(callback): null), + get: () => watchlocationchange.locationchangecallbacks }); -watchlocationchange.interval = watchlocationchange.interval || setInterval(() => watchlocationchange('href'), 1000); +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) { @@ -1835,7 +2092,8 @@ String.prototype.toCaps = String.prototype.toCaps || function toCaps(all) { 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?) + 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+/); @@ -1851,10 +2109,11 @@ String.prototype.toCaps = 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, $$, $_) => $1[0].toUpperCase() + $1.slice(1, $1.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; }; @@ -1995,7 +2254,7 @@ String.prototype.toCaps = 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); @@ -2005,6 +2264,20 @@ String.prototype.toCaps = String.prototype.toCaps || function toCaps(all) { Object.entries(attributes).forEach( ([name, value]) => (/^(on|(?:(?:inner|outer)(?:HTML|Text)|textContent|class(?:List|Name)|value)$)/.test(name))? + (typeof value == 'string' && /^on/.test(name))? + (() => { + try { + /* Can't make a new function(eval) */ + element[name] = new Function('', value); + } catch (__error) { + try { + /* Not a Chrome (extension) state */ + chrome.tabs.getCurrent(tab => chrome.tabs.executeScript(tab.id, { code: `document.furnish.__cache__ = () => {${ value }}` }, __cache__ => element[name] = __cache__[0] || parent.furnish.__cache__ || value)); + } catch (_error) { + throw __error, _error; + } + } + })(): element[name] = value: element.setAttribute(name, value) );