MiM / BLU Streaming Links

// ==UserScript==
// @name         BLU Streaming Links
// @description  Show streaming links on movie pages
// @version      2.3
// @author       MiM
// @Credits      SB100
// @copyright    2023
// @license      MIT
// @include      https://blutopia.cc/torrents/*
// @include      https://blutopia.cc/requests/*
// @include      https://blutopia.cc/mediahub/movies/*
// @grant        GM_xmlhttpRequest
// @connect      apis.justwatch.com
// @icon         https://blutopia.cc/favicon.ico
// @updateURL    https://openuserjs.org/meta/MiM/BLU_Streaming_Links.meta.js
// @downloadURL  https://openuserjs.org/install/MiM/BLU_Streaming_Links.user.js
// ==/UserScript==

// ==OpenUserJS==
// @author MiM
// ==/OpenUserJS==

/* jshint esversion: 6 */

/**
 * =============================
 * ADVANCED OPTIONS
 * =============================
 */

/**
 * A map of BLU Langauges to JustWatch locales.
 * If a language doesn't match the locale you are in, please update it to retrieve local prices
 */
var LANGUAGE_MAP = {
  'English AU': 'en_AU',
  'English CA': 'en_CA',
  'English GB': 'en_GB',
  'English NZ': 'en_NZ',
  'English PH': 'en_PH',
  'English SG': 'en_SG',
  'English US': 'en_US',
  'English ZA': 'en_ZA',
  'Default': 'en_US',
}

// Save a list of all the possible ones.
var href_lang_tags = [];

/**
 * Messages to show when all versions (SD, HD, 4K) or a film are found in a specific category
 */
const TEXT_ALL_EDITIONS_AVAILABLE = {
  'STREAM': 'All editions available',
  'RENT': 'All editions available',
  'BUY': 'All editions available'
}

/**
 * Error messages to show when a version is missing from one of the categories
 */
const TEXT_NO_LINK_FOUND = {
  'STREAM': 'No links found',
  'RENT': 'No links found',
  'BUY': 'No links found'
}

/**
 * Force the streaming panel to always start in a specific state (opened or closed). Otherwise, the toggle state will be remembered, and used on other movie pages.
 * Allowed enums: 'none', 'open', 'close'
 */
const SETTING_FORCE_PANEL_DEFAULT_STATE = 'none';

/**
 * Sets whether the locale selector should be dimmed or not. Will become full opacity on hover
 */
const SETTING_LANG_SELECT_DIMMED = true;

// How long to cache per-locale providers for. 24 hours is the default.
const CACHE_TIME = 1000 * 60 * 60 * 24;

// We try matching the movie name as well as the release year, but sometimes the year is off. Increasing this number, will allow
// search to match films in a wider range of years.
// e.g. Avengers Endgame [2019] with a searchYearModifier of 1 will match years 2018, 2019 and 2020.
// Set to 0 to be strict on release years
const searchYearModifier = 1;

/**
 * =============================
 * END ADVANCED OPTIONS
 * DO NOT MODIFY BELOW THIS LINE
 * =============================
 */

/**
 * Query the JustWatch API
 */
function query(path, method, params = {}) {
  let resolver;
  let rejecter;
  const p = new Promise((resolveFn, rejectFn) => {
    resolver = resolveFn;
    rejecter = rejectFn;
  });

  const paramStr = new URLSearchParams(params).toString();
  const obj = {
    method,
    timeout: 10000,
    onloadstart: () => {},
    onload: (response) => resolver(response),
    onerror: (response) => rejecter(response),
    ontimeout: (response) => rejecter(response)
  }

  const final = method === 'post' ? Object.assign({}, obj, {
    url: `https://apis.justwatch.com/contentpartner/v2/content${path}`,
    headers: {
      "Content-Type": "application/json"
    },
    data: JSON.stringify(params),
  }) : Object.assign({}, obj, {
    url: `https://apis.justwatch.com/contentpartner/v2/content${path}${paramStr.length > 0 ? `?${paramStr}` : ''}`,
  });
  GM_xmlhttpRequest(final);
  return p;
}

const imdb = document.getElementsByClassName("meta__imdb")[0].getElementsByClassName("meta-id-tag")[0].href.replace("https://www.imdb.com/title/", "");

/**
 * Find a specific Film from the JustWatch API
 */
var imdbID = null;
var jwType = null;
var jwLocaleList = null;
async function queryFind(movieTitle, releaseYear, locale) {
  let result = {}

  const json = JSON.parse(result.response);
  // Needs some update for TV too.  TV will use class="tags" and valud "TV Show" (needs a trim)

  /*
  const filtered = json.items.filter(
      item => (item.object_type === (isTV ? 'show' : 'movie') &&
          (item.original_release_year - searchYearModifier <= releaseYear &&
          item.original_release_year + searchYearModifier >= releaseYear) ||
           releaseYear == null) ||
      item.id == jwID
  );
  if (filtered.length > 0) {
      const lowercaseMovieTitle = movieTitle.toLowerCase();
      for (let i = 0, len = filtered.length; i < len; i += 1) {
          const lowercaseFilteredTitle = filtered[i].title.toLowerCase();
          if (lowercaseFilteredTitle === lowercaseMovieTitle || filtered[i].id === jwID) {
              jwID = filtered[i].id;
              jwType = filtered[i].object_type;
              const jwFullPath = filtered[i].full_path;
              if (!jwLocaleList) {
                  const jwLocales = await findLocales(jwFullPath);
                  if (jwLocales.status === 200) {
                      let jwLocalesJson = JSON.parse(jwLocales.response);
                      jwLocaleList = jwLocalesJson.href_lang_tags.sort(function(a, b) {
                          let aName = localeToName(a.locale);
                          let bName = localeToName(b.locale);
                          if (aName < bName) {
                              return -1;
                          }
                          if (aName > bName) {
                              return 1;
                          }
                          return 0;
                      });
                      updateLanguageMap(jwLocaleList);
                  }
              }
              return filtered[i];
          }
      }
  }*/

  return null;
}

function updateLanguageMap(locales) {
  // Clear the language map and set accordingly.
  LANGUAGE_MAP = {}
  locales.forEach(function (currentValue, index, arr) {
    let localeName = currentValue.locale;
    localeName = localeToName(localeName);
    if (!LANGUAGE_MAP.hasOwnProperty(localeName)) {
      LANGUAGE_MAP[localeName] = currentValue.locale;
    }
  });
}

function localeToName(loc) {
  if (!loc) { // Sanity check for null
    return loc;
  }
  // Language replace, start of string.
  const languageCodeRegex = /^[a-z]{2}_/;
  let languageCode = loc.match(languageCodeRegex);
  if (languageCode) {
    languageCode = languageCode[0].replace('_', '');
    if (languageCode in LANGUAGE_CODES) {
      loc = loc.replace(`${languageCode}_`, `${LANGUAGE_CODES[languageCode]} _`);
    }
  }
  // Country replace, end of string.
  const countryCodeRegex = /_[A-Z]{2}$/;
  let countryCode = loc.match(countryCodeRegex);
  if (countryCode) {
    countryCode = countryCode[0].replace('_', '');
    if (countryCode in COUNTRY_CODES) {
      loc = loc.replace(`_${countryCode}`, `- ${COUNTRY_CODES[countryCode]}`);
    }
  }
  return loc;
}
const COUNTRY_CODES = { //ISO 3166-1 (Alpha-2 code)
  'AF': 'Afghanistan',
  'AL': 'Albania',
  'DZ': 'Algeria',
  'AS': 'American Samoa',
  'AD': 'Andorra',
  'AO': 'Angola',
  'AI': 'Anguilla',
  'AQ': 'Antarctica',
  'AG': 'Antigua and Barbuda',
  'AR': 'Argentina',
  'AM': 'Armenia',
  'AW': 'Aruba',
  'AU': 'Australia',
  'AT': 'Austria',
  'AZ': 'Azerbaijan',
  'BS': 'Bahamas',
  'BH': 'Bahrain',
  'BD': 'Bangladesh',
  'BB': 'Barbados',
  'BY': 'Belarus',
  'BE': 'Belgium',
  'BZ': 'Belize',
  'BJ': 'Benin',
  'BM': 'Bermuda',
  'BT': 'Bhutan',
  'BO': 'Bolivia',
  'BA': 'Bosnia and Herzegovina',
  'BW': 'Botswana',
  'BR': 'Brazil',
  'IO': 'British Indian Ocean Territory',
  'VG': 'British Virgin Islands',
  'BN': 'Brunei',
  'BG': 'Bulgaria',
  'BF': 'Burkina Faso',
  'BI': 'Burundi',
  'KH': 'Cambodia',
  'CM': 'Cameroon',
  'CA': 'Canada',
  'CV': 'Cape Verde',
  'KY': 'Cayman Islands',
  'CF': 'Central African Republic',
  'TD': 'Chad',
  'CL': 'Chile',
  'CN': 'China',
  'CX': 'Christmas Island',
  'CC': 'Cocos Islands',
  'CO': 'Colombia',
  'KM': 'Comoros',
  'CK': 'Cook Islands',
  'CR': 'Costa Rica',
  'HR': 'Croatia',
  'CU': 'Cuba',
  'CW': 'Curacao',
  'CY': 'Cyprus',
  'CZ': 'Czech Republic',
  'CD': 'Democratic Republic of the Congo',
  'DK': 'Denmark',
  'DJ': 'Djibouti',
  'DM': 'Dominica',
  'DO': 'Dominican Republic',
  'TL': 'East Timor',
  'EC': 'Ecuador',
  'EG': 'Egypt',
  'SV': 'El Salvador',
  'GQ': 'Equatorial Guinea',
  'ER': 'Eritrea',
  'EE': 'Estonia',
  'ET': 'Ethiopia',
  'FK': 'Falkland Islands',
  'FO': 'Faroe Islands',
  'FJ': 'Fiji',
  'FI': 'Finland',
  'FR': 'France',
  'PF': 'French Polynesia',
  'GA': 'Gabon',
  'GM': 'Gambia',
  'GE': 'Georgia',
  'DE': 'Germany',
  'GH': 'Ghana',
  'GI': 'Gibraltar',
  'GR': 'Greece',
  'GL': 'Greenland',
  'GD': 'Grenada',
  'GU': 'Guam',
  'GT': 'Guatemala',
  'GG': 'Guernsey',
  'GN': 'Guinea',
  'GW': 'Guinea-Bissau',
  'GY': 'Guyana',
  'HT': 'Haiti',
  'HN': 'Honduras',
  'HK': 'Hong Kong',
  'HU': 'Hungary',
  'IS': 'Iceland',
  'IN': 'India',
  'ID': 'Indonesia',
  'IR': 'Iran',
  'IQ': 'Iraq',
  'IE': 'Ireland',
  'IM': 'Isle of Man',
  'IL': 'Israel',
  'IT': 'Italy',
  'CI': 'Ivory Coast',
  'JM': 'Jamaica',
  'JP': 'Japan',
  'JE': 'Jersey',
  'JO': 'Jordan',
  'KZ': 'Kazakhstan',
  'KE': 'Kenya',
  'KI': 'Kiribati',
  'XK': 'Kosovo',
  'KW': 'Kuwait',
  'KG': 'Kyrgyzstan',
  'LA': 'Laos',
  'LV': 'Latvia',
  'LB': 'Lebanon',
  'LS': 'Lesotho',
  'LR': 'Liberia',
  'LY': 'Libya',
  'LI': 'Liechtenstein',
  'LT': 'Lithuania',
  'LU': 'Luxembourg',
  'MO': 'Macau',
  'MK': 'Macedonia',
  'MG': 'Madagascar',
  'MW': 'Malawi',
  'MY': 'Malaysia',
  'MV': 'Maldives',
  'ML': 'Mali',
  'MT': 'Malta',
  'MH': 'Marshall Islands',
  'MR': 'Mauritania',
  'MU': 'Mauritius',
  'YT': 'Mayotte',
  'MX': 'Mexico',
  'FM': 'Micronesia',
  'MD': 'Moldova',
  'MC': 'Monaco',
  'MN': 'Mongolia',
  'ME': 'Montenegro',
  'MS': 'Montserrat',
  'MA': 'Morocco',
  'MZ': 'Mozambique',
  'MM': 'Myanmar',
  'NA': 'Namibia',
  'NR': 'Nauru',
  'NP': 'Nepal',
  'NL': 'Netherlands',
  'AN': 'Netherlands Antilles',
  'NC': 'New Caledonia',
  'NZ': 'New Zealand',
  'NI': 'Nicaragua',
  'NE': 'Niger',
  'NG': 'Nigeria',
  'NU': 'Niue',
  'NP': 'North Korea',
  'MP': 'Northern Mariana Islands',
  'NO': 'Norway',
  'OM': 'Oman',
  'PK': 'Pakistan',
  'PW': 'Palau',
  'PS': 'Palestine',
  'PA': 'Panama',
  'PG': 'Papua New Guinea',
  'PY': 'Paraguay',
  'PE': 'Peru',
  'PH': 'Philippines',
  'PN': 'Pitcairn',
  'PL': 'Poland',
  'PT': 'Portugal',
  'PR': 'Puerto Rico',
  'QA': 'Qatar',
  'CG': 'Republic of the Congo',
  'RE': 'Reunion',
  'RO': 'Romania',
  'RU': 'Russia',
  'RW': 'Rwanda',
  'BL': 'Saint Barthelemy',
  'SH': 'Saint Helena',
  'KN': 'Saint Kitts and Nevis',
  'LC': 'Saint Lucia',
  'MF': 'Saint Martin',
  'PM': 'Saint Pierre and Miquelon',
  'VC': 'Saint Vincent and the Grenadines',
  'WS': 'Samoa',
  'SM': 'San Marino',
  'ST': 'Sao Tome and Principe',
  'SA': 'Saudi Arabia',
  'SN': 'Senegal',
  'RS': 'Serbia',
  'SC': 'Seychelles',
  'SL': 'Sierra Leone',
  'SG': 'Singapore',
  'SX': 'Sint Maarten',
  'SK': 'Slovakia',
  'SI': 'Slovenia',
  'SB': 'Solomon Islands',
  'SO': 'Somalia',
  'ZA': 'South Africa',
  'KR': 'South Korea',
  'SS': 'South Sudan',
  'ES': 'Spain',
  'LK': 'Sri Lanka',
  'SD': 'Sudan',
  'SR': 'Suriname',
  'SJ': 'Svalbard and Jan Mayen',
  'SZ': 'Swaziland',
  'SE': 'Sweden',
  'CH': 'Switzerland',
  'SY': 'Syria',
  'TW': 'Taiwan',
  'TJ': 'Tajikistan',
  'TZ': 'Tanzania',
  'TH': 'Thailand',
  'TG': 'Togo',
  'TK': 'Tokelau',
  'TO': 'Tonga',
  'TT': 'Trinidad and Tobago',
  'TN': 'Tunisia',
  'TR': 'Turkey',
  'TM': 'Turkmenistan',
  'TC': 'Turks and Caicos Islands',
  'TV': 'Tuvalu',
  'VI': 'U.S. Virgin Islands',
  'UG': 'Uganda',
  'UA': 'Ukraine',
  'AE': 'United Arab Emirates',
  'GB': 'United Kingdom',
  'US': 'United States',
  'UY': 'Uruguay',
  'UZ': 'Uzbekistan',
  'VU': 'Vanuatu',
  'VA': 'Vatican',
  'VE': 'Venezuela',
  'VN': 'Vietnam',
  'WF': 'Wallis and Futuna',
  'EH': 'Western Sahara',
  'YE': 'Yemen',
  'ZM': 'Zambia',
  'ZW': 'Zimbabwe'
};

const LANGUAGE_CODES = { //639-1
  'ab': 'Abkhazian',
  'aa': 'Afar',
  'af': 'Afrikaans',
  'ak': 'Akan',
  'sq': 'Albanian',
  'am': 'Amharic',
  'ar': 'Arabic',
  'an': 'Aragonese',
  'hy': 'Armenian',
  'as': 'Assamese',
  'av': 'Avaric',
  'ae': 'Avestan',
  'ay': 'Aymara',
  'az': 'Azerbaijani',
  'bm': 'Bambara',
  'ba': 'Bashkir',
  'eu': 'Basque',
  'be': 'Belarusian',
  'bn': 'Bengali',
  'bi': 'Bislama',
  'bs': 'Bosnian',
  'br': 'Breton',
  'bg': 'Bulgarian',
  'my': 'Burmese',
  'ca': 'Catalan',
  'ch': 'Chamorro',
  'ce': 'Chechen',
  'ny': 'Chichewa, Chewa, Nyanja',
  'zh': 'Chinese',
  'cv': 'Chuvash',
  'kw': 'Cornish',
  'co': 'Corsican',
  'cr': 'Cree',
  'hr': 'Croatian',
  'cs': 'Czech',
  'da': 'Danish',
  'dv': 'Divehi',
  'nl': 'Dutch, Flemish',
  'dz': 'Dzongkha',
  'en': 'English',
  'eo': 'Esperanto',
  'et': 'Estonian',
  'ee': 'Ewe',
  'fo': 'Faroese',
  'fj': 'Fijian',
  'fi': 'Finnish',
  'fr': 'French',
  'ff': 'Fulah',
  'gl': 'Galician',
  'ka': 'Georgian',
  'de': 'German',
  'el': 'Greek',
  'gn': 'Guarani',
  'gu': 'Gujarati',
  'ht': 'Haitian',
  'ha': 'Hausa',
  'he': 'Hebrew',
  'hz': 'Herero',
  'hi': 'Hindi',
  'ho': 'Hiri Motu',
  'hu': 'Hungarian',
  'ia': 'Interlingua',
  'id': 'Indonesian',
  'ie': 'Interlingue',
  'ga': 'Irish',
  'ig': 'Igbo',
  'ik': 'Inupiaq',
  'io': 'Ido',
  'is': 'Icelandic',
  'it': 'Italian',
  'iu': 'Inuktitut',
  'ja': 'Japanese',
  'jv': 'Javanese',
  'kl': 'Kalaallisut',
  'kn': 'Kannada',
  'kr': 'Kanuri',
  'ks': 'Kashmiri',
  'kk': 'Kazakh',
  'km': 'Central Khmer',
  'ki': 'Kikuyu, Gikuyu',
  'rw': 'Kinyarwanda',
  'ky': 'Kirghiz, Kyrgyz',
  'kv': 'Komi',
  'kg': 'Kongo',
  'ko': 'Korean',
  'ku': 'Kurdish',
  'kj': 'Kuanyama',
  'la': 'Latin',
  'lb': 'Luxembourgish',
  'lg': 'Ganda',
  'li': 'Limburgan',
  'ln': 'Lingala',
  'lo': 'Lao',
  'lt': 'Lithuanian',
  'lu': 'Luba-Katanga',
  'lv': 'Latvian',
  'gv': 'Manx',
  'mk': 'Macedonian',
  'mg': 'Malagasy',
  'ms': 'Malay',
  'ml': 'Malayalam',
  'mt': 'Maltese',
  'mi': 'Maori',
  'mr': 'Marathi',
  'mh': 'Marshallese',
  'mn': 'Mongolian',
  'na': 'Nauru',
  'nv': 'Navajo',
  'nd': 'North Ndebele',
  'ne': 'Nepali',
  'ng': 'Ndonga',
  'nb': 'Norwegian Bokmål',
  'nn': 'Norwegian Nynorsk',
  'no': 'Norwegian',
  'ii': 'Sichuan Yi, Nuosu',
  'nr': 'South Ndebele',
  'oc': 'Occitan',
  'oj': 'Ojibwa',
  'cu': 'Church Slavic',
  'om': 'Oromo',
  'or': 'Oriya',
  'os': 'Ossetian, Ossetic',
  'pa': 'Punjabi, Panjabi',
  'pi': 'Pali',
  'fa': 'Persian',
  'pl': 'Polish',
  'ps': 'Pashto, Pushto',
  'pt': 'Portuguese',
  'qu': 'Quechua',
  'rm': 'Romansh',
  'rn': 'Rundi',
  'ro': 'Romanian',
  'ru': 'Russian',
  'sa': 'Sanskrit',
  'sc': 'Sardinian',
  'sd': 'Sindhi',
  'se': 'Northern Sami',
  'sm': 'Samoan',
  'sg': 'Sango',
  'sr': 'Serbian',
  'gd': 'Gaelic',
  'sn': 'Shona',
  'si': 'Sinhala',
  'sk': 'Slovak',
  'sl': 'Slovenian',
  'so': 'Somali',
  'st': 'Southern Sotho',
  'es': 'Spanish',
  'su': 'Sundanese',
  'sw': 'Swahili',
  'ss': 'Swati',
  'sv': 'Swedish',
  'ta': 'Tamil',
  'te': 'Telugu',
  'tg': 'Tajik',
  'th': 'Thai',
  'ti': 'Tigrinya',
  'bo': 'Tibetan',
  'tk': 'Turkmen',
  'tl': 'Tagalog',
  'tn': 'Tswana',
  'to': 'Tonga',
  'tr': 'Turkish',
  'ts': 'Tsonga',
  'tt': 'Tatar',
  'tw': 'Twi',
  'ty': 'Tahitian',
  'ug': 'Uighur',
  'uk': 'Ukrainian',
  'ur': 'Urdu',
  'uz': 'Uzbek',
  've': 'Venda',
  'vi': 'Vietnamese',
  'vo': 'Volapük',
  'wa': 'Walloon',
  'cy': 'Welsh',
  'wo': 'Wolof',
  'fy': 'Western Frisian',
  'xh': 'Xhosa',
  'yi': 'Yiddish',
  'yo': 'Yoruba',
  'za': 'Zhuang, Chuang',
  'zu': 'Zulu'
};

/**
 * Find all the locales that have it.
 * Input is the uri (locale/type/name)
 */
function findLocales(uri) {
  let params = new URLSearchParams("");
  params.set('include_children', 'true');
  params.set('path', uri);
  params.set('token', "ABCdef12");
  let url = `https://apis.justwatch.com/content/urls?${params.toString()}?token=ABCdef12`;
  //`https://apis.justwatch.com/contentpartner/v2/content/countries?language=en&token=ABCdef12`
  const headers = {
    "Content-Type": "application/json"
  };
  let resolver;
  let rejecter;
  const p = new Promise((resolveFn, rejectFn) => {
    resolver = resolveFn;
    rejecter = rejectFn;
  });
  const final = GM_xmlhttpRequest({
    method: "GET",
    url: url,
    headers: headers,
    onload: (response) => resolver(response),
    onerror: (response) => rejecter(response),
    ontimeout: (response) => rejecter(response)
  });
  return p;
}
let media_type = "movie";

function findByIMDb(id, locale) {
  let url = `https://apis.justwatch.com/contentpartner/v2/content/offers/object_type/${media_type}/id_type/imdb/id/${imdb}/locale/${locale}?language=en&token=ABCdef12`;
  console.log(url)
  const headers = {
    "Content-Type": "application/json"
  };
  let resolver;
  let rejecter;
  const p = new Promise((resolveFn, rejectFn) => {
    resolver = resolveFn;
    rejecter = rejectFn;
  });
  const final = GM_xmlhttpRequest({
    method: "GET",
    url: url,
    headers: headers,
    onload: (response) => resolver(response),
    onerror: (response) => rejecter(response),
    ontimeout: (response) => rejecter(response)
  });
  return p;
}

/**
 * Find all Providers from the JustWatch API
 */
async function queryProviders(locale) {
  const result = await query(`/providers/all/locale/${locale}`, 'get', {
    token: 'ABCdef12'
  });
  if (result.status !== 200) {
    return null;
  }
  const json = JSON.parse(result.response);
  if (!json) {
    return null;
  }

  return json.reduce((result, provider) => {
    result[provider.id] = {
      name: provider.clear_name,
      icon: provider.icon_url
    };
    return result;
  }, {});
}

/**
 * Try parsing a string into JSON, otherwise fallback
 */
function JsonParseWithDefault(s, fallback = null) {
  try {
    return JSON.parse(s);
  }
  catch (e) {
    return fallback;
  }
}

/**
 * Get all settings stored in localStorage for this script
 */
function getSettings() {
  const settings = window.localStorage.getItem('streamingLinkSettings');
  return JsonParseWithDefault(settings || {}, {});
}

/**
 * Set a setting into localStorage for this script
 */
function setSetting(name, value) {
  const json = getSettings();
  json[name] = value;
  window.localStorage.setItem('streamingLinkSettings', JSON.stringify(json));
}

/**
 * Cache / Retrieve the list of Providers from the JustWatch API
 * Cache time is controlled by the CACHE_TIME variable at the top of the script
 */
async function getProviders(locale) {
  const sortedKeys = Object
    .keys(window.localStorage)
    .filter(key => key.startsWith(`justWatchProviders-${locale}-`))
    .sort((a, b) => {
      const aTime = parseInt(a.replace(`justWatchProviders-${locale}-`, ''), 10);
      const bTime = parseInt(b.replace(`justWatchProviders-${locale}-`, ''), 10);
      return aTime - bTime;
    });

  for (let i = 0, sortedKeysLength = sortedKeys.length; i < sortedKeysLength; i += 1) {
    const key = sortedKeys[i];
    const keyTime = parseInt(key.replace(`justWatchProviders-${locale}-`, ''), 10);

    // last entry, and we're still in the cache period
    if (i === sortedKeysLength - 1 && (new Date).getTime() - keyTime < CACHE_TIME) {
      return JsonParseWithDefault(window.localStorage.getItem(key));
    }

    window.localStorage.removeItem(key);
  }

  // if we didn't find an entry, query the api and store in localStorage
  const providers = await queryProviders(locale);
  if (providers && Object.keys(providers).length > 0) {
    window.localStorage.setItem(`justWatchProviders-${locale}-${(new Date).getTime()}`, JSON.stringify(providers));
    return providers;
  }

  // nothing found =(
  return null;
}

/**
 * Create a full icon url from JustWatch
 */
function makeIconUrl(providerId, providers) {
  const iconUrl = providers[providerId] && providers[providerId].icon;
  return iconUrl.replace('{profile}', 's100')
}

/**
 * Format a number with a symbol, and approproate locale separators
 */
function formatCurrency(num, currency = 'USD', locale = 'en-US') {
  return new Intl.NumberFormat(locale.replace('_', '-'), {
    style: 'currency',
    currency
  }).format(num);
}

/**
 * Parse the Movie name and year from the movie page
 */
function getMovieNameAndYear() {
  let metaInfo = document.getElementsByClassName("meta")[0];
  let movieHeading = metaInfo.getElementsByClassName("meta__title-link")[0];
  let movieInfo = movieHeading.getElementsByTagName("h1");
  let movieName = null;
  let year = null;
  let last_paran = -1;
  if (movieInfo.length > 0) {
    movieName = movieInfo[0].innerHTML;
    movieName = movieName.replace('&amp;', '&');
    movieName = movieName.replace('&lt;', '<').replace('&gt;', '>');
    last_paran = movieName.lastIndexOf("(");
    year = movieName.slice(last_paran + 1, movieName.lastIndexOf(")"))
    if (year == '') {
      year = null;
    }
    movieName = movieName.slice(0, last_paran)
    movieName = movieName.trim();
  }

  return {
    movieName,
    year
  }
}

/**
 * Turn a monetization type into a short string to show on BLU
 */
function monetizationToShort(monetization) {
  switch (monetization.toLowerCase()) {
    case 'flatrate':
      return 'Sub';
    case 'free':
      return 'Free';
    case 'ads':
      return 'Ads';
    default:
      '???';
  }
}

/**
 * Get the default open state of the streaming panel
 */
function getInitialOpenState() {
  if (SETTING_FORCE_PANEL_DEFAULT_STATE === 'open') return true;
  if (SETTING_FORCE_PANEL_DEFAULT_STATE === 'close') return false;

  const settings = getSettings();
  return settings.toggleOpen ? true : false;
}

/**
 * Find the appropriate locale for a langauge listed in the Movie Info panel.
 */
function getLangaugeLocale() {
  const movieInfo = document.querySelector('#movieinfo');
  const sections = movieInfo && movieInfo.querySelectorAll('.panel__body > div');
  const langs = sections && Array.from(sections).filter(div => div.innerText.startsWith('Language'));
  if (!langs || langs.length === 0) {
    return "en_US";
  }

  const langArray = langs[0].innerText.replace('Language: ', '').split(', ');
  for (let i = 0, len = langArray.length; i < len; i += 1) {
    const lang = langArray[i];
    if (Object.prototype.hasOwnProperty.call(LANGUAGE_MAP, lang)) {
      return LANGUAGE_MAP[lang];
    }
  }

  return LANGUAGE_MAP.Default;
}

/**
 * Decides which side message to show depending on the messageSlug passed in
 */
function createSideMessage(name, messageSlug, missing = []) {
  switch (messageSlug) {
    case 'LOADING':
      return `<div class="streaming__missing streaming__missing--grey">Loading ...</div>`;
    case 'NO_MOVIE_YEAR':
      return `<div class="streaming__missing streaming__missing--red">Could not parse movie name and/or year</div>`;
    case 'NO_PROVIDER':
      return `<div class="streaming__missing streaming__missing--red">Could not find JustWatch providers</div>`;
    case 'NO_ITEMS':
      return `<div class="streaming__missing streaming__missing--gold">${TEXT_NO_LINK_FOUND[name]}</div>`;
    case 'MISSING':
      return `<div class="streaming__missing streaming__missing--red">Missing: ${missing.join(', ')}</div>`;
    case 'ALL_AVAILABLE':
      return `<div class="streaming__missing streaming__missing--green">${TEXT_ALL_EDITIONS_AVAILABLE[name]}</div>`;
    default:
      return `<div class="streaming__missing streaming__missing--red">Unknown Status</div>`;
  }
}

/**
 * Continuously check the requests page to see if the user has filled in a name and year for the movie
 */
function checkChange() {
  let lastTitle;
  let lastYear;
  const inputTitle = document.querySelector('#title');
  const inputYear = document.querySelector('#year');

  const checkRecreate = () => {
    const title = inputTitle.value;
    const year = inputYear.value;
    const yearInt = parseInt(year, 10);

    if (title === lastTitle && year === lastYear) {
      return;
    }

    lastTitle = title;
    lastYear = year;

    if (title.length > 0 && year.length === 4 && !Number.isNaN(yearInt)) {
      const selector = document.querySelector('#streaming__select');
      createPanelBody(selector.value, title, yearInt);
    }
  };

  if (!inputTitle || !inputYear) {
    return
  }

  setInterval(() => {
    checkRecreate();
  }, 2000);
}

function DeleteKeys(myObj, array) {
  for (let index = 0; index < array.length; index++) {
    delete myObj[array[index]];
  }
  return myObj;
}

/**
 * Create a row in the new streamable panel
 */
function createRow(name, messageSlug, locale, items, providers) {
  let missing = ['SD', 'HD', '4K'];
  const byProvider = items.reduce((results, item) => {
    if (!results[item.provider_id]) results[item.provider_id] = [];
    results[item.provider_id].push(item);
    return results;
  }, {});

  const built = Object
    .keys(byProvider)
    // some providers are missing from the provider list. We can't show them, and JustWatch doesn't either, so let's remove them
    .filter(providerId => typeof providers[providerId] !== 'undefined')
    // sort alphabetically
    .sort((a, b) => {
      const aProvider = providers[a].name;
      const bProvider = providers[b].name;

      if (aProvider > bProvider) return 1;
      if (bProvider > aProvider) return -1;
      return 0;
    })
    .map(providerId => {
      const item = byProvider[providerId];
      const types = item.map(i => `<li>${i.retail_price ? formatCurrency(i.retail_price, i.currency, locale) : monetizationToShort(item[0].monetization_type)} <span style="color: #C8AF4B;">${i.presentation_type.toUpperCase()}</span></li>`).join('');

      // remove types that are present from the 'missing' array
      item.forEach(i => {
        missing = missing.filter(m => m !== i.presentation_type.toUpperCase());
      });
      return `<a class="streaming__item" href="${item[0].urls.standard_web}" target="_blank" rel="noopener noreferrer">
    <img class="streaming__img" src="${makeIconUrl(providerId, providers)}" title="${providers[providerId] && providers[providerId].name || 'Unknown'}" />
    <ul class="list list--unstyled" style="list-style-type: none; padding: 8px; margin: 0; text-align: right">${types}</ul>
</a>`;
    });

  let slug = messageSlug;
  if (!slug) {
    slug = (items.length === 0 ? 'NO_ITEMS' : (missing.length > 0 ? 'MISSING' : 'ALL_AVAILABLE'));
  }

  /*
     switch (slug) {
        case 'NO_MOVIE_YEAR':
        case 'NO_PROVIDER':
        case 'NO_ITEMS':
             LANGUAGE_MAP = Object.keys(LANGUAGE_MAP).reduce(function (filtered, key) {
                 if (LANGUAGE_MAP[key] != locale) filtered[key] = LANGUAGE_MAP[key];
                 return filtered;
             }, {});
            break;
        default:
            break;
    }
    */

  return `<div class="streaming__name">
    ${createSideMessage(name, slug, missing)}
${name}
</div>
<div class="streaming__row">${built.join('')}</div>`;
}

/**
 * Create the footer with the select drop down, and a note on which editions are available on BLU
 */
function createFooter(locale, justWatchObj, movieName, year) {

  const selector = document.createElement('select');
  selector.id = 'streaming__select';
  selector.setAttribute("class", "vscomp-toggle-button");
  selector.onchange = (event) => {
    createPanelBody(event.target.value, movieName, year)
  }
  Object.keys(LANGUAGE_MAP).forEach(lang => {
    const option = document.createElement('option');
    option.value = LANGUAGE_MAP[lang];
    option.innerText = `${lang} (${LANGUAGE_MAP[lang]})`;
    option.selected = LANGUAGE_MAP[lang] === locale;

    selector.appendChild(option);
  });

  const selectorDiv = document.createElement('div');
  selectorDiv.classList.add('streaming__footer-locale', SETTING_LANG_SELECT_DIMMED ? 'streaming__footer-locale--dimmed' : null);
  selectorDiv.appendChild(document.createTextNode('Locale: '));
  selectorDiv.appendChild(selector);

  const infoDiv = document.createElement('div');
  infoDiv.classList.add('streaming__footer-info');
  if (movieName && year) {
    const searchInfo = `BLU: ${movieName} [${year}]
JustWatch: ${justWatchObj.title && justWatchObj.original_release_year ? `${justWatchObj.title} [${justWatchObj.original_release_year}]` : 'Not found'}`;

    infoDiv.innerHTML = `<span title="${searchInfo}">search info</span> |
            <a target="_blank" rel="noopener noreferrer" href="https://justwatch.com${justWatchObj.full_path ? justWatchObj.full_path.replace(/(\/[^/]+)(.*)/, "$1") : '/us'}/search?q=${encodeURIComponent(movieName + ' ' + year)}">
                search link
        </a>`;
  }

  const footer = document.createElement('div');
  footer.classList.add('streaming__footer');
  footer.prepend(infoDiv);
  footer.prepend(selectorDiv);

  return footer;
}

/**
 * Create the rows and footer in the panel body
 */
function createRows(panelBody, messageSlug, locale, justWatchObj, providers, movieName, year) {
  const rows = `${createRow('STREAM', messageSlug, locale, justWatchObj.offers.filter(offer => ['flatrate', 'ads', 'free'].includes(offer.monetization_type)), providers)}
${createRow('RENT', messageSlug, locale, justWatchObj.offers.filter(offer => offer.monetization_type === 'rent'), providers)}
${createRow('BUY', messageSlug, locale, justWatchObj.offers.filter(offer => offer.monetization_type === 'buy'), providers)}`;

  panelBody.innerHTML = rows;
  panelBody.appendChild(createFooter(locale, justWatchObj, movieName, year));
}

/**
 * Query JustWatch and create the rows
 */
async function createPanelBody(forcedLocale = null, movieName = null, year = null) {
  const defaultJustWatchObj = {
    offers: []
  };

  let justWatchObj;
  let locale = LANGUAGE_MAP.Default;

  const panel = document.querySelector('#panel__streaming');

  if (!panel) {
    return;
  }

  if (panel.dataset.hasRun === '1' && forcedLocale === null) {
    return;
  }

  const body = panel.querySelector('.panel__body');
  createRows(body, 'LOADING', forcedLocale || locale, defaultJustWatchObj, null, null, null);

  if (!movieName || !year) {
    const {
      movieName: parsedMovieName,
      year: parsedYear
    } = getMovieNameAndYear();
    if (!parsedMovieName) { //|| !parsedYear) {
      createRows(body, 'NO_MOVIE_YEAR', forcedLocale || locale, defaultJustWatchObj, null, null, null);
      return;
    }

    movieName = parsedMovieName;
    year = parsedYear;
  }

  let isTV = false;
  try {
    isTV = document.getElementsByClassName("torrent__category-link")[0].innerHTML.trim() == "TV Show";
  }
  catch {
    try {
      isTV = document.getElementsByClassName("request__category")[0].getElementsByTagName("span")[0].innerHTML.trim() == "TV Show";
    }
    catch {
      isTV = document.getElementsByClassName("meta-id-tag")[0].href.includes("tv");
    }
  };
  try {
    isTV = document.getElementsByClassName("torrent-search--list__category")[0].getElementsByTagName("img")[0].alt == "TV Show";
  }
  catch {}
  if (isTV) {
    media_type = "show";
  }

  locale = "en_US"

  if (forcedLocale === null) {
    const parsedLocale = getLangaugeLocale();
    justWatchObj = await findByIMDb(imdb, locale);
    if ((!justWatchObj || !justWatchObj.offers) && parsedLocale !== locale) {
      locale = parsedLocale;
      justWatchObj = await findByIMDb(imdb, locale);
    }
  }
  else {
    justWatchObj = await findByIMDb(imdb, forcedLocale);
  }

  if (justWatchObj) {
    justWatchObj = JSON.parse(justWatchObj.responseText)
  }

  // assign a default if we couldn't query the api
  if (!justWatchObj || !justWatchObj.offers || justWatchObj.offers.length === 0) {
    justWatchObj = defaultJustWatchObj;
  }

  const providers = await getProviders(forcedLocale || locale);
  if (!providers) {
    createRows(body, 'NO_PROVIDER', forcedLocale || locale, defaultJustWatchObj, null, null, null);
    return;
  }

  const jwLocales = await findLocales(justWatchObj.full_path);
  if (jwLocales.status === 200) {
    let jwLocalesJson = JSON.parse(jwLocales.response);
    jwLocaleList = jwLocalesJson.href_lang_tags.sort(function (a, b) {
      let aName = localeToName(a.locale);
      let bName = localeToName(b.locale);
      if (aName < bName) {
        return -1;
      }
      if (aName > bName) {
        return 1;
      }
      return 0;
    });
    updateLanguageMap(jwLocaleList);
  }

  // build rows and mark as run
  createRows(body, null /* auto selected in the function*/ , forcedLocale || locale, justWatchObj, providers, movieName, year);
  panel.dataset.hasRun = 1;
}

/**
 * Create the streamable panel container
 */
function createPanel() {
  const isOpen = getInitialOpenState();
  const requestsPage = document.URL.indexOf("/requests/") >= 0;
  const similarPage = document.URL.indexOf("/similar/") >= 0;
  const mediahubPage = document.URL.indexOf("mediahub/") >= 0;
  const blockSingle = requestsPage || similarPage || mediahubPage;
  const panel = document.createElement('section');
  panel.id = 'panel__streaming';
  panel.classList.add('panel');
  //${blockSingle ? '<div class="block single">' : ''}
  panel.innerHTML = `
    <div class="panelV2">
      <header class="panel__header">
        <h2 class="panel__heading">
           <i class="fas fa-cloud"></i>
           Streaming
           &nbsp;&nbsp;&nbsp;
           <a id="panel__toggle" class="panel__heading__toggler" style="font-size: 0.9em;" title="Toggle" href="#streaming">(${isOpen ? 'Hide' : 'Show'} links)</a>
        </h2>
      </header>
      <div class="panel__body bbcode-rendered">
        <div class="panel__body ${isOpen ? '' : 'hidden'}">
        </div>
      </div>
  </div>`;
  //${blockSingle ? '</div>' : ''}

  if (window.location.href.indexOf("requests") > -1) {
    let firstBlock = document.getElementsByClassName("panelV2")[1];
    firstBlock.after(panel);
  }
  else if (window.location.href.indexOf("similar") > -1) {
    //let firstBlock = document.getElementsByClassName("block single")[0];
    //firstBlock.after(panel);
    let num_panels = document.getElementsByClassName("panelV2").length - 1
    let firstBlock = document.getElementsByClassName("panelV2")[num_panels];
    firstBlock.after(panel);
  }
  else {
    let num_panels = document.getElementsByClassName("panelV2").length - 3
    let firstBlock = document.getElementsByClassName("panelV2")[num_panels];
    firstBlock.after(panel);
  }

  if (isOpen) {
    createPanelBody();
  }

  const panelToggle = document.querySelector('#panel__toggle');
  panelToggle.addEventListener('click', () => {
    const panelBody = document.querySelector('#panel__streaming .panel__body');

    if (panelBody.classList.contains('hidden')) {
      panelBody.classList.remove('hidden');
      panelToggle.innerText = '(Hide links)';
      setSetting('toggleOpen', true);
      createPanelBody();
    }
    else {
      panelBody.classList.add('hidden');
      panelToggle.innerText = '(Show links)';
      setSetting('toggleOpen', false);
    }
  });
}

/**
 * All the custom styling that powers the loadout. BEM FTW.
 */
function createStyleTag() {
  const css = `.streaming__row {
    display: flex;
    flex-direction: row;
    margin-bottom: 10px;
    border-bottom: 1px solid #666;
    padding-bottom: 10px;
    flex-wrap: wrap;
}

.streaming__name {
    font-weight: bold;
    text-transform: uppercase;
}

.streaming__footer {
    display: flex;
    justify-content: space-between;
}

.streaming__footer-locale {
    display: inline-block;
    vertical-align: middle;
}

.streaming__footer-locale--dimmed {
    opacity: 0.5;
}

.streaming__footer-locale:hover {
    opacity: 1;
}

.streaming__footer-info {
    color: #999;
    cursor: help;
    line-height: 18px;
}

.streaming__footer-info a {
    color: #999;
}

.streaming__missing {
    font-weight: bold;
    float: right;
    text-align: right;
    text-transform: none;
    font-size: 0.9em;
}

.streaming__missing-footer {
    line-height: 18px;
}

.streaming__missing--red {
    color: #ff0000;
}

.streaming__missing--green {
    color: #009000;
}

.streaming__missing--gold {
    color: #C8AF4B;
}

.streaming__missing--grey {
    color: #CCC;
}

.streaming__item {
    display: block;
    margin-right: 15px;
    margin-top: 10px;
    text-align: center;
}

.streaming__img {
    width: 50px;
    border-radius: 10px;
}`;

  const style = document.createElement('style');
  style.type = 'text/css';
  style.appendChild(document.createTextNode(css));

  document.head.appendChild(style);
}

// Main script runner
(async function () {
  'use strict';

  createStyleTag();
  createPanel();

  checkChange();
})();