Chaturbate Enhancer

Enhances Chaturbate by adding multiple new features.

As of 02.10.2022. See ბოლო ვერსია.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name             Chaturbate Enhancer
// @name:de          Chaturbate Enhancer
// @name:es          Chaturbate Enhancer
// @name:es-CO       Chaturbate Enhancer
// @name:it          Chaturbate Enhancer
// @name:fr          Chaturbate Enhancer
// @name:fr-CA       Chaturbate Enhancer
// @name:ru          Chaturbate Enhancer
// @name:tr          Chaturbate Enhancer
// @name:ro          Chaturbate Enhancer
// @name:no          Chaturbate Enhancer
// @name:nl          Chaturbate Enhancer
// @name:pl          Chaturbate Enhancer
// @name:ja          Chaturbate Enhancer
// @name:el          Chaturbate Enhancer
// @name:hu          Chaturbate Enhancer
// @name:fi          Chaturbate Enhancer
// @name:ar          Chaturbate Enhancer
// @name:hi          Chaturbate Enhancer
// @name:id          Chaturbate Enhancer
// @name:ko          Chaturbate Enhancer
// @name:pt-PT       Chaturbate Enhancer
// @name:pt-BR       Chaturbate Enhancer
// @name:zh          Chaturbate Enhancer
// @name:zh-CN       Chaturbate Enhancer
// @name:zh-TW       Chaturbate Enhancer
// @name:cs          Chaturbate Enhancer
// @name:sk          Chaturbate Enhancer
// @name:sl          Chaturbate Enhancer
// @name:sv          Chaturbate Enhancer
// @name:sr          Chaturbate Enhancer
// @name:af          Chaturbate Enhancer
// @name:sq          Chaturbate Enhancer
// @name:hy          Chaturbate Enhancer
// @name:be          Chaturbate Enhancer
// @name:bg          Chaturbate Enhancer
// @name:da          Chaturbate Enhancer
// @name:et          Chaturbate Enhancer
// @name:he          Chaturbate Enhancer
// @name:hr          Chaturbate Enhancer
// @name:fa          Chaturbate Enhancer
// @name:ur          Chaturbate Enhancer
// @name:bn          Chaturbate Enhancer
// @name:th          Chaturbate Enhancer
// @name:eo          Chaturbate Enhancer
// @name:ug          Chaturbate Enhancer
// @name:vi          Chaturbate Enhancer
// @description      Enhances Chaturbate by adding multiple new features.
// @description:de   Verbessert Chaturbate durch Hinzufügen mehrerer neuer Funktionen.
// @description:es   Mejora Chaturbate al agregar múltiples funciones nuevas.
// @description:es-CO Mejora Chaturbate al agregar múltiples funciones nuevas.
// @description:it   Migliora Chaturbate aggiungendo più nuove funzionalità.
// @description:fr   Améliore Chaturbate en ajoutant plusieurs nouvelles fonctionnalités.
// @description:fr-CA Améliore Chaturbate en ajoutant plusieurs nouvelles fonctionnalités.
// @description:ru   Улучшает Chaturbate, добавляя несколько новых функций.
// @description:tr   Birden çok yeni özellik ekleyerek Chaturbate'i geliştirir.
// @description:ro   Îmbunătățește Chaturbate prin adăugarea de mai multe funcții noi.
// @description:no   Forbedrer Chaturbate ved å legge til flere nye funksjoner.
// @description:nl   Verbetert Chaturbate door meerdere nieuwe functies toe te voegen.
// @description:pl   Ulepsza Chaturbate, dodając wiele nowych funkcji.
// @description:ja   複数の新機能を追加して Chaturbate を強化します。
// @description:el   Βελτιώνει το Chaturbate προσθέτοντας πολλές νέες δυνατότητες.
// @description:hu   Több új funkció hozzáadásával továbbfejleszti a Chaturbate szolgáltatást.
// @description:fi   Parantaa Chaturbatea lisäämällä useita uusia ominaisuuksia.
// @description:ar   يعزز Chaturbate عن طريق إضافة ميزات جديدة متعددة.
// @description:hi   कई नई सुविधाओं को जोड़कर Chaturbate को बेहतर बनाता है।
// @description:id   Meningkatkan Chaturbate dengan menambahkan beberapa fitur baru.
// @description:ko   여러 새로운 기능을 추가하여 Chaturbate를 향상시킵니다.
// @description:pt-PT Aprimora o Chaturbate adicionando vários novos recursos.
// @description:pt-BR Aprimora o Chaturbate adicionando vários novos recursos.
// @description:zh   通过添加多个新功能来增强 Chaturbate。
// @description:zh-CN 通过添加多个新功能来增强 Chaturbate。
// @description:zh-TW 通过添加多个新功能来增强 Chaturbate。
// @description:cs   Vylepšuje Chaturbate přidáním několika nových funkcí.
// @description:sk   Vylepšuje Chaturbate pridaním viacerých nových funkcií.
// @description:sl   Izboljša Chaturbate z dodajanjem več novih funkcij.
// @description:sv   Förbättrar Chaturbate genom att lägga till flera nya funktioner.
// @description:sr   Побољшава Цхатурбате додавањем више нових функција.
// @description:af   Verbeter Chaturbate deur verskeie nuwe kenmerke by te voeg.
// @description:sq   Përmirëson Chaturbate duke shtuar veçori të shumta të reja.
// @description:hy   Ընդլայնում է Chaturbate-ը՝ ավելացնելով բազմաթիվ նոր հնարավորություններ:
// @description:be   Паляпшае Chaturbate шляхам дадання некалькіх новых функцый.
// @description:bg   Подобрява Chaturbate чрез добавяне на множество нови функции.
// @description:da   Forbedrer Chaturbate ved at tilføje flere nye funktioner.
// @description:et   Täiustab Chaturbate'i, lisades mitu uut funktsiooni.
// @description:he   משפר את Chaturbate על ידי הוספת תכונות חדשות מרובות.
// @description:hr   Poboljšava Chaturbate dodavanjem više novih značajki.
// @description:fa   Chaturbate را با افزودن چندین ویژگی جدید تقویت می کند.
// @description:ur   متعدد نئی خصوصیات شامل کرکے Chaturbate کو بہتر بناتا ہے۔
// @description:bn   একাধিক নতুন বৈশিষ্ট্য যোগ করে Chaturbate উন্নত করে।
// @description:th   ปรับปรุง Chaturbate ด้วยการเพิ่มคุณสมบัติใหม่หลายอย่าง
// @description:eo   Plibonigas Chaturbate aldonante plurajn novajn funkciojn.
// @description:ug   كۆپ خىل يېڭى ئىقتىدارلارنى قوشۇش ئارقىلىق Chaturbate نى كۈچەيتىدۇ.
// @description:vi   Cải thiện Chaturbate bằng cách thêm nhiều tính năng mới.
// @version          1.3.11
// @author           MoonDivision
// @license          CC-BY-ND-4.0
// @copyright        MoonDivision (https://sleazyfork.org/en/users/884016-moondivision)
// @namespace        https://sleazyfork.org/en/users/884016-moondivision
// @homepage         https://sleazyfork.org/en/scripts/441079-chaturbate-enhancer
// @supportURL       https://sleazyfork.org/en/scripts/441079-chaturbate-enhancer/feedback
// @contributionURL  https://cb-enh.improper.dev/contribute
// @icon             https://www.google.com/s2/favicons?sz=32&domain=chaturbate.com
// @icon64           https://www.google.com/s2/favicons?sz=64&domain=chaturbate.com
// @match            https://chaturbate.com/*
// @match            https://*.chaturbate.com/*
// @connect          camschedule.com
// @connect          onechance.onelove.workers.dev
// @connect          cb-enh.improper.dev
// @grant            GM_addStyle
// @grant            GM_addElement
// @grant            GM_xmlhttpRequest
// @grant            GM_registerMenuCommand
// @grant            GM_unregisterMenuCommand
// @grant            GM_setClipboard
// @require          https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
// @require          https://cdn.jsdelivr.net/npm/hls.js@1/dist/hls.min.js
// @run-at           document-body
// @noframes
// ==/UserScript==

(function() {
'use strict';

let style = `
/* Hide media overlays */
.photoVideoDetailSection img {
	filter: unset !important;
}

.userUpload div {
	background: none !important;
}

.psContainer .lockOverlayBg, .smContainer .lockOverlayBg {
	display: none !important;
}

.userUpload img[src$="lock.svg"] {
	display: none !important;
}

/* Hide ads */
.ad, .vote-banner {
	display: none !important;
}

.cb-enh-avatar {
	margin-left: 10px;
	border: 1px solid #bfbfbf;
	width: 150px;
	height: 150px;
	background-color: #ebebeb;
	margin-bottom: 5px;
	background-size: 100% 100%;
	position: relative;
}

.darkmode .cb-enh-avatar {
	border-color: #2d3e50;
	background-color: #202c39;
}

.cb-enh-avatar, .cb-enh-avatar img {
	border-radius: 150px;
}

.cb-enh-avatar img {
	width: 100%;
	height: 100%;
	opacity: 0;

	position: absolute;
	left: 0;
	top: 0;

	-webkit-user-drag: none;
	-webkit-app-region: no-drag;
	user-drag: none;
	app-region: no-drag;

	pointer-events: none;

	-webkit-touch-callout: none;
	-webkit-user-select: none;
	-khtml-user-select: none;
	-moz-user-select: none;
	-ms-user-select: none;
	user-select: none;
}

.cb-enh-footer {
	font-size: 14px;
	color: #341b00;
	font-weight: bold;
}

.darkmode .cb-enh-footer {
	color: #efefef;
}

.cb-enh-footer a {
	color: inherit !important;
	text-decoration: underline;
}

/* Enlarge media in bio */
tr:not(.smContainer) .contentText .previewBorder {
	width: 190px;
	height: 135px;
}

tr:not(.smContainer) .contentText .tokenText {
	top: 118px !important;
	right: 5px !important;
}

/* Detach floaters in "about" */
tr:not(.smContainer):not(.psContainer) .contentText img, tr:not(.smContainer):not(.psContainer) .contentText li, tr:not(.smContainer):not(.psContainer) .contentText a, tr:not(.smContainer):not(.psContainer) .contentText p {
	position: unset !important;
}

.cb-enh-video {
	max-width: 900px;
	margin: 0px;
	padding: 0px;
	width: 100%;
	height: 100%;
	object-fit: contain;
	background-color: rgba(0, 0, 0, 0);
	display: inline;
	border: 0;
	outline: 0;
	border-radius: 4px;
}

.cb-enh-video::-webkit-media-controls-play-button {
	display: none;
}

.cb-enh-video::-webkit-media-controls-timeline {
	display: none;
}

.cb-enh-video::-webkit-media-controls-current-time-display {
	display: none;
}

.cb-enh-video::-webkit-media-controls-timeline-container {
	display: none;
}

.cb-enh-video::-webkit-media-controls-time-remaining-display {
	display: none;
}

.cb-enh-schedule-frame {
	width: 100%;
	height: 350px;
	border: 0;
}

.cb-enh-chat-frame {
	width: 100%;
	max-width: 1400px;
	height: 700px;
	border: 0;
	border-radius: 4px;
}

#cb-enh-inac-load-chat {
	cursor: pointer;
}
`;

GM_addStyle(style);
let lang = $('html').attr('lang');
if(lang !== 'en') {
	loadLocales(lang);
}

$(document).ready(function() {
	if(window.location.pathname.startsWith('/roomlogin/')) {
		enhancePasswordedRoom();
	}
	else if('initialRoomDossier' in unsafeWindow) {
		enhanceRoom();
	}

	if(!$('#id_animate_thumbnails').is(':checked')) {
		$(document).on('mouseenter', '.room_list_room img, .roomElement img, .roomCard img', function(e) {
			e.preventDefault();
			e.stopImmediatePropagation();

			if(window.currentHoverInterval) {
				clearInterval(window.currentHoverInterval);
				window.currentHoverInterval = null;
			}

			updateRoomThumb($(this));
			window.currentHoverInterval = setInterval(() => {
				updateRoomThumb($(this));
			}, 100);
		});

		$(document).on('mouseleave', '.room_list_room img, .roomElement img, .roomCard img', function(e) {
			e.preventDefault();
			e.stopImmediatePropagation();

			if(window.currentHoverInterval) {
				clearInterval(window.currentHoverInterval);
				window.currentHoverInterval = null;
			}
		});
	}
});

function loadLocales(lang) {
	GM_xmlhttpRequest({
		method: 'GET',
		url: 'https://cb-enh.improper.dev/locale/' + lang + '.json',
		timeout: 60*1*1000,
		onload: function(resp) {
			let data;
			try {
				data = JSON.parse(resp.responseText);
			}
			catch(SyntaxError) {
				return;
			}

			window.locales = data['locales'];
		}
	});
}

function localizeStrings() {
	if(!window.locales) {
		return;
	}

	$('.ce-loc').each(function() {
		let v = $(this).data('ce-loc');
		if(window.locales[v]) {
			$(this).text(window.locales[v]);
		}
	});
}

function updateRoomThumb($el) {
	// Stop CB script from executing something on image load
	$el[0].onload = null;

	let uname = $el.parent().data('room');
	$el.attr('src', 'https://roomimg.stream.highwebmedia.com/minifwap/' + uname + '.jpg?' + Math.random());
}

document.cookie = 'noads=1; expires=Sun, 1 Jan 9999 00:00:00 UTC; path=/';
document.cookie = 'agreeterms=1; expires=Sun, 1 Jan 9999 00:00:00 UTC; path=/';
document.cookie = 'fromaffiliate=1; expires=Sun, 1 Jan 9999 00:00:00 UTC; path=/';
document.cookie = 'affkey="eJyrViopylayUlBKzctQ0lFQSkxLA/HMiwsM03KTQCIFIL6RIYhZBGKCGCUgRnpRoQGIk5wLVuKXZBFZpVQLAEdlFCg="; expires=Sun, 1 Jan 9999 00:00:00 UTC; path=/';

document.cookie = 'noads=1; expires=Sun, 1 Jan 9999 00:00:00 UTC; path=/; domain=.chaturbate.com';
document.cookie = 'agreeterms=1; expires=Sun, 1 Jan 9999 00:00:00 UTC; path=/; domain=.chaturbate.com';
document.cookie = 'fromaffiliate=1; expires=Sun, 1 Jan 9999 00:00:00 UTC; path=/; domain=.chaturbate.com';
document.cookie = 'affkey="eJyrViopylayUlBKzctQ0lFQSkxLA/HMiwsM03KTQCIFIL6RIYhZBGKCGCUgRnpRoQGIk5wLVuKXZBFZpVQLAEdlFCg="; expires=Sun, 1 Jan 9999 00:00:00 UTC; path=/; domain=.chaturbate.com';

function enhanceRoom(ajaxTransition=false) {
	GM_unregisterMenuCommand('Get video source URL');
	$('.cb-enh-row').remove();

	if(!ajaxTransition) {
		let cFunc = function() {
			if(!window.currentBroadcaster) {
				return;
			}

			GM_xmlhttpRequest({
				method: 'GET',
				url: 'https://onechance.onelove.workers.dev/?https://chaturbate.com/api/chatvideocontext/' + window.currentBroadcaster + '/',
				headers: {
					'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
					'Referer': 'https://chaturbate.com/' + window.currentBroadcaster + '/',
				},
				timeout: 60*1*1000,
				onload: function(resp) {
					let data;
					try {
						data = JSON.parse(resp.responseText);
					}
					catch(SyntaxError) {
						return;
					}

					if(!('hls_source' in data) || data['hls_source'] === '') {
						alert('No video URL.')
						return;
					}

					GM_setClipboard(data['hls_source'], 'text');
					alert(data['hls_source'] + '\n\n(copied to clipboard)');
				}
			});
		}
		GM_registerMenuCommand('Get video source URL', cFunc, 'g');
	}

	if(unsafeWindow.initialRoomDossier === '') {
		// initialRoomDossier is set but is empty
		// room might be banned or blocked for user
		enhanceInaccessibleRoom();
		return;
	}

	let intv = setInterval(function() {
		if($('video.vjs-tech').length === 0) {
			return;
		}

		// Make clicking on live video feed don't pause it anymore
		$('video.vjs-tech').on('pause', function() {
			try {
				$('video.vjs-tech')[0].play();
			} catch(err) {}
		});
		clearInterval(intv);

		// Watch for AJAX page transition
		let currentUsername = $('a.nextCamBgColor')[0].getAttribute('href').slice(6, -1);
		let pageTransitionIntv = setInterval(function() {
			let uname = $('a.nextCamBgColor')[0].getAttribute('href').slice(6, -1);
			if(currentUsername != uname) {
				clearInterval(pageTransitionIntv);
				enhanceRoom(true);
				currentUsername = uname;
			}	
		}, 25);

		// Add Picture in Picture button to the player
		if(unsafeWindow.videoJsPlayer) {
			let PictureInPictureToggle = videojs.getComponent('pictureInPictureToggle');
			if(PictureInPictureToggle) {
				let pictureInPictureToggle = new PictureInPictureToggle(unsafeWindow.videoJsPlayer, {});
				unsafeWindow.videoJsPlayer.getChild('ControlBar').addChild(pictureInPictureToggle);
			}
		}
	}, 25);

	let userData;
	let broadcasterName;
	if(!ajaxTransition) {
		userData = JSON.parse(unsafeWindow.initialRoomDossier);
		broadcasterName = userData.broadcaster_username;
	}
	else {
		broadcasterName = $('a.nextCamBgColor')[0].getAttribute('href').slice(6, -1);
	}
	window.currentBroadcaster = broadcasterName;

	let lang = $('html').attr('lang');
	let intervalId = setInterval(() => {
		let $table = $('.BioContents > div > table');
		if($table.length === 0) {
			return;
		}
		clearInterval(intervalId);

		// Add offline avatar
		let $offlineNotice = $('.offlineRoomNotice');
		if($offlineNotice.length > 0) {
			insertRoomAv($offlineNotice, broadcasterName);
		}

		let $divSchedule = addBioRow('Schedule', false, '<div id="cb-enh-iframe"></div>');
		if(userData && userData.room_status === 'offline') {
			addBioRow('Last Subject', true, userData.room_title);
		}
		let $divRegion = addBioRow('Region', false, '<a href=""></a>');
		let $divOnlineFor = addBioRow('Online For', false);

		GM_xmlhttpRequest({
			method: 'GET',
			url: 'https://camschedule.com/api/room/' + broadcasterName + '?lang=' + lang,
			timeout: 60*2*1000,
			onload: function(resp) {
				let data;
				try {
					data = JSON.parse(resp.responseText);
				}
				catch(SyntaxError) {
					return;
				}

				// Populate "region" row
				if(data['region'] !== '') {
					let href = '';
					if(data['region_id'] == 0) {
						href = '/asian-cams/';
					}
					else if(data['region_id'] == 1) {
						href = '/euro-russian-cams/';
					}
					else if(data['region_id'] == 2) {
						href = '/north-american-cams/';
					}
					else if(data['region_id'] == 3) {
						href = '/south-american-cams/';
					}
					else if(data['region_id'] == 4) {
						href = '/other-region-cams/';
					}

					let elA = $divRegion.children('.cb-enh-row-value').children('a')[0];
					elA.innerHTML = data['region'];
					elA.href = href;
					$divRegion.show();
				}

				// Populate "online for" row
				if(data['online_for'] && data['online_for'] !== '') {
					$divOnlineFor.children('.cb-enh-row-value')[0].innerHTML = data['online_for'];
					$divOnlineFor.show();
				}

				// Add schedule
				if(data['has_schedule']) {
					let darkMode = $('body').hasClass('darkmode') ? 1 : 0;
					let iframeWrapper = document.getElementById('cb-enh-iframe');
					GM_addElement(iframeWrapper, 'iframe', {
						src: 'https://camschedule.com/embed/schedule/' + broadcasterName + '?dark=' + darkMode + '&lang=' + lang,
						class: 'cb-enh-schedule-frame'
					});
					$divSchedule.show();
				}

				localizeStrings();
			}
		});
	}, 500);

	// Insert model avatar on private etc. video board
	if(window.intvUpdateAvatarInPrivBoard) {
		clearInterval(window.intvUpdateAvatarInPrivBoard);
	}

	window.intvUpdateAvatarInPrivBoard = setInterval(() => {
		let $el = $('#VideoPanel div[ts]').eq(0);
		if($el.data('cb-enh-av')) {
			return;
		}

		let $div = $el.find('div:first-child').eq(0);
		if($div.length === 0) {
			return;
		}

		$el.data('cb-enh-av', true);
		let $avDiv = insertRoomAv($div, broadcasterName);
		$avDiv.css('margin', '0 auto');
		$avDiv.css('margin-bottom', '10px');
	}, 500);
}

function enhanceInaccessibleRoom() {
	let lang = $('html').attr('lang');

	// Display video of inaccessible room
	let $baseRoomContentDiv =  $("div.BaseRoomContents div")
	if($baseRoomContentDiv.length === 0) {
		return;
	}

	$baseRoomContentDiv = $baseRoomContentDiv.eq(0);
	if($baseRoomContentDiv.text().indexOf("Access denied") !== 0) {
		return;
	}

	$baseRoomContentDiv.append('<br><span class="ce-loc" data-ce-loc="try_load">Chaturbate Enhancer will try to load video and bio of this room.</span><br><br>');

	let $langForm = $("form[action='/set_language/'] input[name='next']");
	if($langForm.length === 0) {
		return;
	}
	let username = $("form[action='/set_language/'] input[name='next']")[0].value.slice(1, -1);
	window.currentBroadcaster = username;

	GM_addStyle(`
		.BaseRoomContents div {
			font-size: 14px !important;
			font-family: UbuntuMedium, Arial, Helvetica, sans-serif;
			font-weight: normal;
		}

		.darkmode .BaseRoomContents {
			border-color: transparent !important;
			background-color: #202c39 !important;
		}

		.ce-row-1 {
			color: #0a5a83;
		}

		.darkmode .ce-row-1 {
			color: white;
		}
	`);

	let $upperHolder = $('<div></div>');
	$baseRoomContentDiv.append($upperHolder);

	let $videoHolder = $('<div></div>');
	$baseRoomContentDiv.append($videoHolder);

	let $upperHolder2 = $('<div></div>');
	$baseRoomContentDiv.append($upperHolder2);

	let $upperHolder3 = $('<div></div>');
	$baseRoomContentDiv.append($upperHolder3);

	let $infoHolder = $('<div></div>');
	$baseRoomContentDiv.append($infoHolder);

	let $infoHolder2 = $('<div></div>');
	$baseRoomContentDiv.append($infoHolder2);

	let $scheduleHolder = $('<div></div>');
	$baseRoomContentDiv.append($scheduleHolder);

	let isOnline = false;

	// video type
	GM_xmlhttpRequest({
		method: 'GET',
		url: 'https://onechance.onelove.workers.dev/?https://chaturbate.com/api/chatvideocontext/' + username + '/',
		headers: {
			'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
			'Referer': 'https://chaturbate.com/' + username + '/',
		},
		timeout: 60*1*1000,
		onload: function(resp) {
			let data;
			try {
				data = JSON.parse(resp.responseText);
			}
			catch(SyntaxError) {
				return;
			}

			if($baseRoomContentDiv.length === 0) {
				return;
			}

			let playVideo = true;
			if(data['room_status'] !== 'public') {
				let $avDiv = $("<div></div>");
				$upperHolder.append($avDiv);
				insertRoomAv($avDiv, username);
				$upperHolder.append('<span class="ce-loc ce-row-1" data-ce-loc="room_status">Room status is</span>: <span class="ce-loc" data-ce-loc="status_' + data['room_status'] + '">' + data['room_status'] + '</span><br>');
				playVideo = false;
			}
			else {
				isOnline = true;
				$("#cb-enh-inac-load-chat").show()
			}

			if(data['hls_source'] === '') {
				playVideo = false;
			}

			if(data['room_title']) {
				let $span;
				if(data['room_status'] !== 'offline') {
					$span = $('<span><span class="ce-loc ce-row-1" data-ce-loc="subject">Subject</span>: <span></span></span>');
				}
				else {
					$span = $('<span><span class="ce-loc ce-row-1" data-ce-loc="last_subject">Last Subject</span>: <span></span></span>');
				}
				$span.find('span').eq(1).text(data['room_title']);
				$upperHolder.append($span);
				$upperHolder.append('<br><br>');
			}

			if(playVideo) {
				let $video = $('<video controls webkit-playsinline playsinline autoplay muted data-listener-count-webkitendfullscreen="1" class="vjs-tech cb-enh-video" id="vjs_video_3_html5_api" tabindex="-1" role="application" poster="https://cbjpeg.stream.highwebmedia.com/stream?room=' + username + '&f=' + Math.random() + '"></video>');
				$videoHolder.append($video);

				$video.on('pause', function() {
					try {
						$video[0].play();
					} catch(err) {}
				});

				$video.on('click', function() {
					try {
						$video[0].play();
					} catch(err) {}
				});

				let hls = new Hls();
				hls.loadSource(data['hls_source']);
				hls.attachMedia($video[0]);
			}

			if(data['age']) {
				$upperHolder3.append('<br><br><span class="ce-loc ce-row-1" data-ce-loc="age">Age</span>: ' + data['age'] + '<br>');
			}

			if(data['broadcaster_gender']) {
				$upperHolder3.append('<span class="ce-loc ce-row-1" data-ce-loc="gender">Gender</span>: <span class="ce-loc" data-ce-loc="gender_' + data['broadcaster_gender'][0] + '">'  + data['broadcaster_gender'] + '</span><br>');
			}

			if(data['num_viewers']) {
				if(data['room_status'] !== 'offline') {
					$upperHolder3.append('<span class="ce-loc ce-row-1" data-ce-loc="viewers">Viewers</span>: ' + data['num_viewers'] + '<br>');
				}
				else {
					$upperHolder3.append('<span class="ce-loc ce-row-1" data-ce-loc="last_viewers">Last Viewers</span>: ' + data['num_viewers'] + '<br>');
				}
			}
			
			if(data['performer_has_fanclub']) {
				$infoHolder2.append('<span class="ce-loc ce-row-1" data-ce-loc="has_fanclub">Has Fanclub</span>: <span class="ce-loc" data-ce-loc="yes">Yes</span><br>');
			}
			else {
				$infoHolder2.append('<span class="ce-loc ce-row-1" data-ce-loc="has_fanclub">Has Fanclub</span>: <span class="ce-loc" data-ce-loc="no">No</span><br>');
			}

			if('satisfaction_score' in data) {
				let sc = data['satisfaction_score'];
				if('percent' in sc && 'up_votes' in sc && 'down_votes' in sc) {
					$infoHolder2.append('<span class="ce-loc ce-row-1" data-ce-loc="satisfaction_score">Satisfaction Score</span>: ' + sc['percent'] + '% (' + sc['up_votes'] + ' <span class="ce-loc" data-ce-loc="up">up</span>, ' + sc['down_votes'] + ' <span class="ce-loc" data-ce-loc="down">down</span>)<br>');
				}
			}

			localizeStrings();
		},
		onerror: function() {
			$upperHolder.append('<br>ERROR: Unable to load video.');
		}
	});

	// Fetch info about room
	GM_xmlhttpRequest({
		method: 'GET',
		url: 'https://camschedule.com/api/room/' + username + '?lang=' + lang,
		timeout: 60*2*1000,
		onload: function(resp) {
			let data;
			try {
				data = JSON.parse(resp.responseText);
			}
			catch(SyntaxError) {
				return;
			}

			let $loadChatHref = $('<a id="cb-enh-inac-load-chat" style="display:none;" class="ce-loc" data-ce-loc="try_load_chat">Click here to try to load chat.</a><br>');
			$upperHolder2.append($loadChatHref);
			if(isOnline) {
				$loadChatHref.show();
			}

			$loadChatHref.on('click', function(e) {
				$loadChatHref.hide();

				e.preventDefault();
				e.stopPropagation();

				$videoHolder.empty();
				GM_addElement($videoHolder[0], 'iframe', {
					src: 'https://onechance.onelove.workers.dev/?https://chaturbate.com/embed/' + username + '/',
					class: 'cb-enh-chat-frame'
				});
			});

			// Populate "region" row
			if(data['region'] !== '') {
				let href = '';
				if(data['region_id'] == 0) {
					href = '/asian-cams/';
				}
				else if(data['region_id'] == 1) {
					href = '/euro-russian-cams/';
				}
				else if(data['region_id'] == 2) {
					href = '/north-american-cams/';
				}
				else if(data['region_id'] == 3) {
					href = '/south-american-cams/';
				}
				else if(data['region_id'] == 4) {
					href = '/other-region-cams/';
				}
				$infoHolder.append('<span class="ce-loc ce-row-1" data-ce-loc="region">Region</span>: <a href="' + href + '">' + data['region'] + '</a><br>');
			}

			// Populate "online for" row
			if(data['online_for'] && data['online_for'] !== '') {
				$infoHolder.append('<span class="ce-loc ce-row-1" data-ce-loc="online_for">Online For</span>: ' + data['online_for'] + '<br>');
			}
			else if(data['last_online_f'] && data['last_online_f'] !== '') {
				$infoHolder.append('<span class="ce-loc ce-row-1" data-ce-loc="last_online">Last Online</span>: ' + data['last_online_f'] + '<br>');
			}

			let info = {
				'real_name': 'Real Name',
				'birthday': 'Birthday',
				'followers_f': 'Followers',
				'location': 'Location',
				'languages': 'Languages',
				'smoke_drink': 'Smoke / Drink',
				'body_type': 'Body Type',
				'body_decorations': 'Body Decorations',
			};

			Object.keys(info).forEach(function(k) {
				let v = info[k];
				if(data[k]) {
					let $span = $('<span><span class="ce-loc ce-row-1" data-ce-loc="' + k + '">' + v + '</span>: <span></span></span>');
					$span.find('span').eq(1).text(data[k]);
					$infoHolder.append($span);
					$infoHolder.append('<br>');
				}
			});

			// Add schedule
			if(data['has_schedule']) {
				$scheduleHolder.append('<span class="ce-loc ce-row-1" data-ce-loc="schedule">Schedule</span>: <br>');
				let darkMode = $('body').hasClass('darkmode') ? 1 : 0;
				GM_addElement($scheduleHolder[0], 'iframe', {
					src: 'https://camschedule.com/embed/schedule/' + username + '?dark=' + darkMode + '&lang=' + lang,
					class: 'cb-enh-schedule-frame'
				});
			}

			localizeStrings();
		}
	});
}

function enhancePasswordedRoom() {
	// @todo
}

function addBioRow(name, visible = true, value = '') {
	let loc = name.replace(' ', '_').toLowerCase();
	let $el = $('<tr class="cb-enh-row" style="' + (visible ? '' : 'display: none; ') + 'font-size: 14px; font-weight: normal; line-height: 15px; vertical-align: top; text-align: left;"><td class="label" style="padding-bottom: 9px; font-family: UbuntuMedium, Arial, Helvetica, sans-serif; height: 16px;"><span><span class="ce-loc" data-ce-loc="' + loc + '">' + name + '</span>:</span></td><td class="contentText cb-enh-row-value" style="font-size: 14px; line-height: 16px; font-family: UbuntuRegular, Arial, Helvetica, sans-serif;">' + value + '</td></tr>');

	let $psContainers = $('.BioContents > div > table > .psContainer');
	let $smContainers = $('.BioContents > div > table > .smContainer');

	if($psContainers.length > 0) {
		$psContainers.last().after($el);
	}
	else if($smContainers.length > 0) {
		$smContainers.last().after($el);
	}
	else {
		$('.BioContents > div > table > tr').slice(-2).first().after($el);
	}

	return $el;
}

function insertRoomAv($div, username) {
	let $avDiv = $('<div class="cb-enh-avatar"></div>');
	$div.prepend($avDiv);
	GM_addElement($('.cb-enh-avatar')[0], 'img', {
		src: 'https://camschedule.com/assets/img/avatar.png',
		alt: '',
		onload: 'this.style.opacity=1'
	});

	GM_addElement($('.cb-enh-avatar')[0], 'img', {
		src: 'https://thumbv.camschedule.com/av/' + username + '.jpg',
		alt: '',
		onload: 'this.style.opacity=1'
	});
	return $avDiv;
}

})();