NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Aither Streaming Links
// @description Show streaming links on movie pages
// @version 1.0
// @author MiM
// @Credits SB100
// @copyright 2025
// @license MIT
// @include https://aither.cc/torrents/*
// @include https://aither.cc/requests/*
// @include https://aither.cc/mediahub/movies/*
// @grant GM_xmlhttpRequest
// @connect apis.justwatch.com
// @icon https://aither.cc/favicon.ico
// @updateURL https://openuserjs.org/meta/MiM/Aither_Streaming_Links.meta.js
// @downloadURL https://openuserjs.org/install/MiM/Aither_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('&', '&');
movieName = movieName.replace('<', '<').replace('>', '>');
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
<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();
})();