// ==UserScript==
// @author Mr. Nope
// @version 1.6
// @name PornHub Plus
// @description A kinder PornHub. Because you're worth it.
// @namespace Nope
// @date 2019-05-27
// @include *pornhub.com*
// @run-at document-start
// @grant none
// @license Public Domain
// @icon https://res-4.cloudinary.com/crunchbase-production/image/upload/c_lpad,h_256,w_256,f_auto,q_auto:eco/v1458758320/ajcppvmug2jcsljnzbmi.jpg
// @grant GM_addStyle
// ==/UserScript==
(() => {
const OPTIONS = {
openWithoutPlaylist: JSON.parse(localStorage.getItem('plus_openWithoutPlaylist')) || false,
showOnlyVerified: JSON.parse(localStorage.getItem('plus_showOnlyVerified')) || false,
showOnlyHd: JSON.parse(localStorage.getItem('plus_showOnlyHd')) || false,
redirectToVideos: JSON.parse(localStorage.getItem('plus_redirectToVideos')) || false,
cinemaModePlayer: JSON.parse(localStorage.getItem('plus_cinemaMode')) || false,
hideWatchedVideos: JSON.parse(localStorage.getItem('plus_hideWatchedVideos')) || false,
hidePlaylistBar: JSON.parse(localStorage.getItem('plus_hidePlaylistBar')) || false,
durationFilter: JSON.parse(localStorage.getItem('plus_durationFilter')) || { max: 0, min: 0 },
durationPresets: [
{ label: 'Micro', min: 0, max: 2 },
{ label: 'Short', min: 3, max: 8 },
{ label: 'Average', min: 8, max: 18 },
{ label: 'Long', min: 18, max: 40 },
{ label: 'Magnum', min: 40, max: 0 }
],
observedAreas: ['.videos-list']
}
/**
* Shared Styles
*/
const sharedStyles = `
/* Our own elements */
.plus-buttons {
background: rgba(27, 27, 27, 0.9);
box-shadow: 0px 0px 12px rgba(20, 111, 223, 0.9);
font-size: 12px;
position: fixed;
bottom: 10px;
padding: 10px 22px 8px 24px;
right: 0;
z-index: 100;
transition: all 0.3s ease;
/* Negative margin-right calculated later based on width of buttons */
}
.plus-buttons:hover {
box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.3);
}
.plus-buttons .plus-button {
margin: 10px 0;
padding: 6px 15px;
border-radius: 4px;
font-weight: 700;
display: block;
position: relative;
text-align: center;
vertical-align: top;
cursor: pointer;
border: none;
text-decoration: none;
}
.plus-buttons a.plus-button {
background: rgb(221, 221, 221);
color: rgb(51, 51, 51);
}
.plus-buttons a.plus-button:hover {
background: rgb(187, 187, 187);
color: rgb(51, 51, 51);
}
.plus-buttons a.plus-button.plus-button-isOn {
background: rgb(20, 111, 223);
color: rgb(255, 255, 255);
}
.plus-buttons a.plus-button.plus-button-isOn:hover {
background: rgb(0, 91, 203);
color: rgb(255, 255, 255);
}
.plus-hidden {
display: none !important;
}
`;
/**
* Color Theme
*/
const themeStyles = `
.plus-buttons {
box-shadow: 0px 0px 12px rgba(255, 153, 0, 0.85);
}
.plus-buttons:hover {
box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.3);
}
.plus-buttons a.plus-button {
background: rgb(47, 47, 47);
color: rgb(172, 172, 172);
}
.plus-buttons a.plus-button:hover {
background: rgb(79, 79, 79);
color: rgb(204, 204, 204);
}
.plus-buttons a.plus-button.plus-button-isOn {
background: rgb(255, 153, 0);
color: rgb(0, 0, 0);
}
.plus-buttons a.plus-button.plus-button-isOn:hover {
background: rgb(255, 153, 0);
color: rgb(255, 255, 255);
}
`;
/**
* Site-Specific Styles
*/
const generalStyles = `
/* Hide elements */
.realsex,
.networkBar,
#welcome,
.sniperModeEngaged,
.footer,
.footer-title,
.ad-link,
.removeAdLink,
.removeAdLink + iframe,
.abovePlayer,
.streamatesModelsContainer,
#headerUpgradePremiumBtn,
#PornhubNetworkBar,
#js-abContainterMain,
#hd-rightColVideoPage > :not(#relatedVideosVPage) {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
height: 0 !important;
width: 0 !important;
}
/* Make "HD" icon more visible on thumbnails */
.hd-thumbnail {
color: #f90 !important;
}
/* Show all playlists without scrolling in "add to" */
.slimScrollDiv {
height: auto !important;
}
#scrollbar_watch {
max-height: unset !important;
}
/* Hide premium video from related videos sidebar */
#relateRecommendedItems li:nth-of-type(5) {
display: none !important;
}
/* Prevent animating player size change on each page load */
#main-container .video-wrapper #player.wide {
transition: none !important;
}
/* Fit more playlists into "add to" popup */
.playlist-menu-addTo {
display: none;
}
.add-to-playlist-menu #scrollThumbs,
.playlist-option-menu #scrollThumbs {
height: 320px !important;
max-height: 35vh !important;
}
.add-to-playlist-menu ul.custom-playlist li {
font-size: 12px;
height: 24px;
}
.add-to-playlist-menu .playlist-menu-createNew {
font-size: 12px !important;
height: 38px !important;
}
.add-to-playlist-menu .playlist-menu-createNew a {
padding-top: 8px !important;
font-weight: 400 !important;
}
/* Hide playlist bar if disabled in options */
.playlist-bar {
display: ${OPTIONS.hidePlaylistBar ? 'none' : 'block'};
}
`;
/**
* Run after page has loaded
*/
window.addEventListener('DOMContentLoaded', () => {
/**
* References to video element and container if they exist on the page
*/
const player = document.querySelector('#player');
const video = document.querySelector('video');
/**
* Creation of option buttons
*/
const scrollButton = document.createElement('a');
const scrollButtonText = document.createElement('span');
const playlistBarButton = document.createElement('a');
const playlistBarButtonText = document.createElement('span');
const playlistBarButtonState = getButtonState(OPTIONS.hidePlaylistBar);
const verifiedButton = document.createElement('a');
const verifiedButtonText = document.createElement('span');
const verifiedButtonState = getButtonState(OPTIONS.showOnlyVerified);
const hideWatchedButton = document.createElement('a');
const hideWatchedButtonText = document.createElement('span');
const hideWatchedButtonState = getButtonState(OPTIONS.hideWatchedVideos);
const hdButton = document.createElement('a');
const hdButtonText = document.createElement('span');
const hdButtonState = getButtonState(OPTIONS.showOnlyHd);
const redirectToVideosButton = document.createElement('a');
const redirectToVideosButtonText = document.createElement('span');
const redirectToVideosButtonState = getButtonState(OPTIONS.redirectToVideos);
const cinemaModeButton = document.createElement('a');
const cinemaModeButtonText = document.createElement('span');
const cinemaModeButtonState = getButtonState(OPTIONS.cinemaModePlayer);
const durationShortButton = document.createElement('a');
const durationShortButtonText = document.createElement('span');
const durationShortButtonState = getButtonState(!OPTIONS.durationFilter.min);
const durationMediumButton = document.createElement('a');
const durationMediumButtonText = document.createElement('span');
const durationMediumButtonState = getButtonState(OPTIONS.durationFilter.min <= 8 && OPTIONS.durationFilter.max >= 20);
const openWithoutPlaylistButton = document.createElement('a');
const openWithoutPlaylistButtonText = document.createElement('span');
const openWithoutPlaylistButtonState = getButtonState(OPTIONS.openWithoutPlaylist);
/**
* Returns an `on` or `off` CSS class name based on the boolean evaluation
* of the `state` parameter, as convenience method when setting UI state.
*/
function getButtonState(state) {
return state ? 'plus-button-isOn' : 'plus-button-isOff';
}
scrollButtonText.textContent = "Scroll to Top";
scrollButtonText.classList.add('text');
scrollButton.appendChild(scrollButtonText);
scrollButton.classList.add('plus-button');
scrollButton.addEventListener('click', () => {
window.scrollTo({ top: 0 });
});
verifiedButtonText.textContent = 'Verified Only';
verifiedButtonText.classList.add('text');
verifiedButton.appendChild(verifiedButtonText);
verifiedButton.classList.add(verifiedButtonState, 'plus-button');
verifiedButton.addEventListener('click', () => {
OPTIONS.showOnlyVerified = !OPTIONS.showOnlyVerified;
localStorage.setItem('plus_showOnlyVerified', OPTIONS.showOnlyVerified);
if (OPTIONS.showOnlyVerified) {
verifiedButton.classList.replace('plus-button-isOff', 'plus-button-isOn');
} else {
verifiedButton.classList.replace('plus-button-isOn', 'plus-button-isOff');
}
filterVideos();
});
hdButtonText.textContent = 'HD Only';
hdButtonText.classList.add('text');
hdButton.appendChild(hdButtonText);
hdButton.classList.add(hdButtonState, 'plus-button');
hdButton.addEventListener('click', () => {
OPTIONS.showOnlyHd = !OPTIONS.showOnlyHd;
localStorage.setItem('plus_showOnlyHd', OPTIONS.showOnlyHd);
if (OPTIONS.showOnlyHd) {
hdButton.classList.replace('plus-button-isOff', 'plus-button-isOn');
} else {
hdButton.classList.replace('plus-button-isOn', 'plus-button-isOff');
}
filterVideos();
});
playlistBarButtonText.textContent = 'Hide Playlist Bar';
playlistBarButtonText.classList.add('text');
playlistBarButton.appendChild(playlistBarButtonText);
playlistBarButton.classList.add(playlistBarButtonState, 'plus-button');
playlistBarButton.addEventListener('click', () => {
OPTIONS.hidePlaylistBar = !OPTIONS.hidePlaylistBar;
localStorage.setItem('plus_hidePlaylistBar', OPTIONS.hidePlaylistBar);
if (OPTIONS.hidePlaylistBar) {
playlistBarButton.classList.replace('plus-button-isOff', 'plus-button-isOn');
} else {
playlistBarButton.classList.replace('plus-button-isOn', 'plus-button-isOff');
}
const playlistBar = document.querySelector('.playlist-bar');
if (playlistBar) {
playlistBar.style.display = OPTIONS.hidePlaylistBar ? 'none' : 'block';
}
});
hideWatchedButtonText.textContent = 'Unwatched Only';
hideWatchedButtonText.classList.add('text');
hideWatchedButton.appendChild(hideWatchedButtonText);
hideWatchedButton.classList.add(hideWatchedButtonState, 'plus-button');
hideWatchedButton.addEventListener('click', () => {
OPTIONS.hideWatchedVideos = !OPTIONS.hideWatchedVideos;
localStorage.setItem('plus_hideWatchedVideos', OPTIONS.hideWatchedVideos);
if (OPTIONS.hideWatchedVideos) {
hideWatchedButton.classList.replace('plus-button-isOff', 'plus-button-isOn');
} else {
hideWatchedButton.classList.replace('plus-button-isOn', 'plus-button-isOff');
}
filterVideos();
});
redirectToVideosButtonText.textContent = 'Redirect Profiles to Uploads';
redirectToVideosButtonText.classList.add('text');
redirectToVideosButton.appendChild(redirectToVideosButtonText);
redirectToVideosButton.classList.add(redirectToVideosButtonState, 'plus-button');
redirectToVideosButton.addEventListener('click', () => {
OPTIONS.redirectToVideos = !OPTIONS.redirectToVideos;
localStorage.setItem('plus_redirectToVideos', OPTIONS.redirectToVideos);
if (OPTIONS.redirectToVideos) {
redirectToVideosButton.classList.replace('plus-button-isOff', 'plus-button-isOn');
} else {
redirectToVideosButton.classList.replace('plus-button-isOn', 'plus-button-isOff');
}
});
durationShortButtonText.textContent = 'Short Videos (< 8 min)';
durationShortButtonText.classList.add('text');
durationShortButton.appendChild(durationShortButtonText);
durationShortButton.classList.add(durationShortButtonState, 'plus-button');
durationShortButton.addEventListener('click', () => {
OPTIONS.durationFilter.min = OPTIONS.durationFilter.min ? 0 : 8;
localStorage.setItem('plus_durationFilter', JSON.stringify(OPTIONS.durationFilter));
if (!OPTIONS.durationFilter.min) {
durationShortButton.classList.replace('plus-button-isOff', 'plus-button-isOn');
filterVideos();
} else {
durationShortButton.classList.replace('plus-button-isOn', 'plus-button-isOff');
filterVideos();
}
});
durationMediumButtonText.textContent = 'Medium Videos (8-20 min)';
durationMediumButtonText.classList.add('text');
durationMediumButton.appendChild(durationMediumButtonText);
durationMediumButton.classList.add(durationMediumButtonState, 'plus-button');
durationMediumButton.addEventListener('click', () => {
OPTIONS.durationFilter.min = OPTIONS.durationFilter.min !== 8 ? 8 : 0;
OPTIONS.durationFilter.max = OPTIONS.durationFilter.max !== 20 ? 20 : 0;
localStorage.setItem('plus_durationFilter', JSON.stringify(OPTIONS.durationFilter));
if (OPTIONS.durationFilter.min === 8 && OPTIONS.durationFilter.max === 20) {
durationMediumButton.classList.replace('plus-button-isOff', 'plus-button-isOn');
filterVideos();
} else {
durationMediumButton.classList.replace('plus-button-isOn', 'plus-button-isOff');
filterVideos();
}
});
cinemaModeButtonText.textContent = 'Cinema Mode';
cinemaModeButtonText.classList.add('text');
cinemaModeButton.appendChild(cinemaModeButtonText);
cinemaModeButton.classList.add(cinemaModeButtonState, 'plus-button');
cinemaModeButton.addEventListener('click', () => {
OPTIONS.cinemaModePlayer = !OPTIONS.cinemaModePlayer;
localStorage.setItem('plus_cinemaMode', OPTIONS.cinemaModePlayer);
if (OPTIONS.cinemaModePlayer) {
cinemaModeButton.classList.replace('plus-button-isOff', 'plus-button-isOn');
} else {
cinemaModeButton.classList.replace('plus-button-isOn', 'plus-button-isOff');
}
});
/**
* Order option buttons in a container
*/
const buttons = document.createElement('div');
const durationFilters = [];
buttons.classList.add('plus-buttons');
buttons.appendChild(scrollButton);
buttons.appendChild(cinemaModeButton);
buttons.appendChild(verifiedButton);
buttons.appendChild(hdButton);
buttons.appendChild(hideWatchedButton);
buttons.appendChild(redirectToVideosButton);
buttons.appendChild(playlistBarButton);
/**
* Generate buttons for filtering by duration. Buttons are created based on
* `OPTIONS.durationPresets`, an array of objects containing max and min duration in minutes,
* and a label (like "Short Videos").
*/
OPTIONS.durationPresets.forEach(preset => {
const button = document.createElement('a');
const buttonText = document.createElement('span');
const buttonState = getButtonState(OPTIONS.durationFilter.min === preset.min &&
OPTIONS.durationFilter.max === preset.max);
buttonText.textContent = `${preset.label} (${preset.min}-${preset.max} mins)`;
buttonText.classList.add('text');
button.appendChild(buttonText);
button.classList.add(buttonState, 'plus-button');
durationFilters.push({
button,
preset
});
});
/**
* We needed access to all buttons, their state, and the duration values, to be able to switch
* all the buttons to off state before we apply the newly selected filters. For simplicity and
* sanity, only one duration range can be selected at a time.
*/
durationFilters.forEach(({ button, preset }) => {
buttons.appendChild(button);
button.addEventListener('click', () => {
durationFilters.forEach(filter => filter.button.classList.replace('plus-button-isOn', 'plus-button-isOff'));
OPTIONS.durationFilter.min = OPTIONS.durationFilter.min === preset.min ? 0 : preset.min;
OPTIONS.durationFilter.max = OPTIONS.durationFilter.max === preset.max ? 0 : preset.max;
localStorage.setItem('plus_durationFilter', JSON.stringify(OPTIONS.durationFilter));
if (OPTIONS.durationFilter.min === preset.min &&
OPTIONS.durationFilter.max === preset.max) {
button.classList.replace('plus-button-isOff', 'plus-button-isOn');
filterVideos();
} else {
button.classList.replace('plus-button-isOn', 'plus-button-isOff');
filterVideos();
}
});
});
document.body.appendChild(buttons); // Button container ready and added to page
/**
* Observe certain areas that should trigger refiltering if nodes are added or removed, so that
* for example when more related videos are loaded into the sidebar, they will be filtered
* according to settings right away. Any selectors in `OPTIONS.observedAreas` will be observed
* if found on page.
*/
OPTIONS.observedAreas.forEach(selector => {
const observableArea = document.querySelector(selector);
if (observableArea) {
/**
* If the element is found on the page, we observe any mutations to it, and if those
* mutations include new nodes we call `filterVideos()`. We could check to make sure the
* nodes are relevant, but it would probably be slower to run than the actual filtering.
*/
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => mutation.addedNodes.length && filterVideos());
});
observer.observe(observableArea, { childList: true, subtree: true }); // Start observing
}
});
/**
* General UI related functions
*/
/**
* Does the CSS changes necessary to enable cinema mode. In case this runs
* immediately on page load, we wait a couple of seconds for the player
* controls to load before telling the cinema mode button to appear active.
*/
function toggleCinemaMode() {
// Add and remove some CSS classes that control cinema view
player.classList.remove('original');
player.classList.add('wide');
document.querySelector('#hd-rightColVideoPage').classList.add('wide');
// Make sure cinema mode button appears as active
setTimeout(() => {
document.querySelector('.mhp1138_cinema').classList.add('mhp1138_active');
}, 2000);
}
/**
* Clicking a video on a playlist page opens it without the playlist at the
* top if the option is enabled.
*/
function updatePlaylistLinks() {
if (OPTIONS.openWithoutPlaylist) {
document.querySelectorAll('#videoPlaylist li a').forEach(link => {
link.href = link.href.replace('pkey', 'nopkey');
});
} else {
document.querySelectorAll('#videoPlaylist li a').forEach(link => {
link.href = link.href.replace('nopkey', 'pkey');
});
}
}
/**
* Allow scrolling the page when mouse hovers playlists in "add to", by
* cloning the playlist scroll container to remove the listeners that
* `preventDefault()`.
*/
function fixScrollContainer(container) {
if (container) {
container.parentNode.replaceChild(container.cloneNode(true), container);
}
}
/**
* Video thumbnail box related functions
*/
/**
* Checks if video box links to a video made by a verified member
*/
function videoIsVerified(box) {
return box.innerHTML.includes('Video of verified member');
}
/**
* Checks if video box links to a HD video
*/
function videoIsHd(box) {
return box.querySelector('.hd-thumbnail');
}
/**
* Checks if the video box has a "watched" label on it (the video has
* already been viewed)
*/
function videoIsWatched(box) {
return box.querySelector('.watchedVideoText');
}
/**
* Checks if video box links to a video that is within the selected duration
* range, if one has been selected in options.
*/
function videoIsWithinDuration(box) {
// Parse integer minutes from video duration text
const mins = parseInt(box.querySelector('.duration').textContent.split(":")[0]);
const minMins = OPTIONS.durationFilter.min;
const maxMins = OPTIONS.durationFilter.max;
// If either max or min duration has been selected
if (minMins || maxMins) {
// If any max duration is set (otherwise defaults to 0 for no max)
const hasMaxDuration = !!maxMins;
// True if the video is shorther than we want (min defaults to 0)
const isBelowMin = mins < minMins;
// True if a max duration is set and the video exceeds it
const isAboveMax = hasMaxDuration && (mins > maxMins - 1);
// One minute negative offset since we ignore any extra seconds
return !isBelowMin && !isAboveMax;
} else {
return true;
}
}
/**
* Resets video thumbnail box to its original visible state.
*/
function resetVideo(box) {
showVideo(box);
}
/**
* Shows the video thumbnail box.
*/
function showVideo(box) {
box.classList.remove('plus-hidden');
}
/**
* Hides the video thumbnail box.
*/
function hideVideo(box) {
box.classList.add('plus-hidden');
}
/**
* Does the required checks to filter out unwanted video boxes according to
* options. Each box is reset to it's original visible state, then it's
* checked against relevant options to determine if it should be hidden or
* stay visible.
*/
function filterVideos() {
document.querySelectorAll('li.videoblock.videoBox').forEach(box => {
const state = {
verified: videoIsVerified(box),
watched: videoIsWatched(box),
hd: videoIsHd(box),
inDurationRange: videoIsWithinDuration(box)
}
const shouldHide =
(OPTIONS.showOnlyHd && !state.hd) ||
(OPTIONS.showOnlyVerified && !state.verified) ||
(OPTIONS.hideWatchedVideos && state.watched) ||
!state.inDurationRange;
// Reset the box to its original visible state so we can focus only on
// what to hide instead of also on what to unhide
resetVideo(box);
if (shouldHide) {
hideVideo(box);
}
});
}
/**
* Initialize video pages (that contain a valid video element)
*/
if (/^http[s]*:\/\/[www.]*pornhub\.com\/view_video.php/.test(window.location.href) && player) {
if (OPTIONS.cinemaModePlayer) {
toggleCinemaMode();
}
// Let us scroll the page despite the mouse pointer hovering over the "Add to" playlist area
const scrollContainer = document.querySelector('#scrollbar_watch');
if (scrollContainer) {
fixScrollContainer(scrollContainer);
}
}
/**
* Initialize any page that contains a video box
*/
if (document.querySelector('.videoBox')) {
setTimeout(() => {
filterVideos();
}, 1000);
}
/**
* Initialize profile pages, channel pages, user pages, star pages
*/
/**
* Redirect profile pages straight to their video uploads page if the setting is
* enabled, except in case we just came from the video page (don't loop back).
*/
if (
/^http[s]*:\/\/[www.]*pornhub\.com\/pornstar\/([^\/]+)$/.test(window.location.href) ||
/^http[s]*:\/\/[www.]*pornhub\.com\/model\/([^\/]+)$/.test(window.location.href)) {
// Stars and models have their own uploads at `/videos/uploads`
if (OPTIONS.redirectToVideos && !/.+\/videos.*/.test(document.referrer)) {
window.location.href = window.location.href + '/videos/upload';
}
} else if (
/^http[s]*:\/\/[www.]*pornhub\.com\/users\/([^\/]+)$/.test(window.location.href) ||
/^http[s]*:\/\/[www.]*pornhub\.com\/channels\/([^\/]+)$/.test(window.location.href)) {
// Users and channels only have `/videos` and not `/videos/uploads`
if (OPTIONS.redirectToVideos && !/.+\/videos.*/.test(document.referrer)) {
window.location.href = window.location.href + '/videos';
}
}
/*
* Add styles
*/
GM_addStyle(sharedStyles);
GM_addStyle(themeStyles);
GM_addStyle(generalStyles);
/*
* Add dynamic styles
*/
const dynamicStyles = `
.plus-buttons {
margin-right: -${buttons.getBoundingClientRect().width - 23}px;
}
.plus-buttons:hover {
margin-right: 0;
}
`;
GM_addStyle(dynamicStyles);
});
})();