Chaturbate Enhancer

ปรับปรุง Chaturbate ด้วยการเพิ่มคุณสมบัติใหม่หลายอย่าง

ของเมื่อวันที่ 10-10-2022 ดู เวอร์ชันล่าสุด

// ==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.12
// @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
// @connect          cb-enh-thumb.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/jquery@3.2.1/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://cb-enh-thumb.improper.dev/av/' + username + '.jpg',
		alt: '',
		onload: 'this.style.opacity=1'
	});
	return $avDiv;
}

})();