Chaturbate Enhancer

Enhances Chaturbate by adding multiple new features.

As of 2022-07-30. See the latest version.

// ==UserScript==
// @name             Chaturbate Enhancer
// @description      Enhances Chaturbate by adding multiple new features.
// @version          1.2.3
// @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://chaturbate.com/in/?tour=JpRf&campaign=Nb8Yz&track=enh-contrib&next=/tipping/purchase_tokens/
// @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
// @grant            GM_addStyle
// @grant            GM_addElement
// @grant            GM_xmlhttpRequest
// @require          https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
// @require          https://cdn.jsdelivr.net/npm/hls.js@1
// @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;
}

.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-inac-load-chat {
	cursor: pointer;
}
`;

GM_addStyle(style);

$(document).ready(function() {
	if('initialRoomDossier' in unsafeWindow) {
		enhanceRoom();
	}

	if(!$('#id_animate_thumbnails').is(':checked')) {
		$(document).on('mouseenter', '.room_list_room img, .roomElement img, .roomCard img', function() {
			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() {
			if(window.currentHoverInterval) {
				clearInterval(window.currentHoverInterval);
				window.currentHoverInterval = null;
			}
		});
	}
});

function updateRoomThumb($el) {
	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) {
	$('.cb-enh-row').remove();
	let lang = $('html').attr('lang');

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

		// Display video of inaccessible room
		let $baseRoomContentDiv =  $("div.BaseRoomContents div")
		if($baseRoomContentDiv.length > 0) {
			if($baseRoomContentDiv.text().indexOf("Access denied") === 0) {
				$baseRoomContentDiv.append("<br>Chaturbate Enhancer will try to display video of this room.<br><br>");

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

					GM_addStyle(`
						.BaseRoomContents div {
							font-size: 14px !important;
						}
					`);

					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(responseDetails) {
							let data;
							try {
								data = JSON.parse(responseDetails.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('Room status is: ' + data['room_status'] + '<br>');
								playVideo = false;
							}
							else {
								isOnline = true;
								$("#cb-enh-inac-load-chat").show()
							}

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

							if(data['room_title']) {
								if(data['room_status'] !== 'offline') {
									$upperHolder.append('Subject: ' + data['room_title'] + '<br><br>');
								}
								else {
									$upperHolder.append('Last Subject: ' + data['room_title'] + '<br><br>');
								}
							}

							let $video;
							if(playVideo) {
								$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>Age: ' + data['age'] + '<br>');
							}

							if(data['broadcaster_gender']) {
								$upperHolder3.append('Gender: ' + data['broadcaster_gender'] + '<br>');
							}

							if(data['num_viewers']) {
								if(data['room_status'] !== 'offline') {
									$upperHolder3.append('Viewers: ' + data['num_viewers'] + '<br>');
								}
								else {
									$upperHolder3.append('Last Viewers: ' + data['num_viewers'] + '<br>');
								}
							}
							
							if(data['performer_has_fanclub']) {
								$infoHolder2.append('Has Fanclub: Yes<br>');
							}
							else {
								$infoHolder2.append('Has Fanclub: No<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('Satisfaction Score: ' + sc['percent'] + '% (' + sc['up_votes'] + ' up, ' + sc['down_votes'] + ' down)<br>');
								}
							}
						},
						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(responseDetails) {
							let data;
							try {
								data = JSON.parse(responseDetails.responseText);
							}
							catch(SyntaxError) {
								return;
							}

							let $loadChatHref = $('<a id="cb-enh-inac-load-chat" style="display:none;">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 + '/',
									style: 'width: 100%; max-width: 1400px; height: 700px; border: 0; border-radius: 4px;',
								});
							});

							// 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('Region: <a href="' + href + '">' + data['region'] + '</a><br>');
							}

							// Populate "online for" row
							if(data['online_for'] && data['online_for'] !== '') {
								$infoHolder.append('Online For: ' + data['online_for'] + '<br>');
							}
							else if(data['last_online'] && data['last_online'] !== '') {
								$infoHolder.append('Last Online: ' + data['last_online'] + '<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]) {
									$infoHolder.append(v + ': ' + data[k] + '<br>');
								}
							});

							// Add schedule
							if(data['has_schedule']) {
								$scheduleHolder.append('Schedule: <br>');
								let darkMode = $('body').hasClass('darkmode') ? 1 : 0;
								GM_addElement($scheduleHolder[0], 'iframe', {
									src: 'https://camschedule.com/embed/schedule/' + username + '?dark=' + darkMode + '&lang=' + lang,
									style: 'width: 100%; height: 350px; border: 0;',
								});
							}
						}
					});
				}
			}
		}
		return;
	}

	let intv = setInterval(function() {
		if($('video.vjs-tech').length > 0) {
			// 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);
		}
	}, 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);
	}

	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(responseDetails) {
				let data;
				try {
					data = JSON.parse(responseDetails.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'] !== '') {
					$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,
						style: 'width: 100%; height: 350px; border: 0;',
					});
					$divSchedule.show();
				}
			}
		});
	}, 500);
}

function addBioRow(name, visible = true, value = '') {
	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>' + name + ':</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) {
	$div.prepend('<div class="cb-enh-avatar"></div>');
	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'
	});
}

})();