// ==UserScript==
// @name Pornhub 助手
// @icon data:image/x-icon;base64,AAABAAEAEBAAAAEACABoBQAAFgAAACgAAAAQAAAAIAAAAAEACAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAAAAAAAAAAABorAAACAwAAwP8AAG+5AAA0VgAAUYkAAInkAAA/aQAAMFEAAJf/AABZlAAAmv8AAGSnAAAaLAAAERwAAKP/AABHZAAABAcAAC0xAACp/wAArP8AALL/AABDcAAAbowAAI/uAAAKEAAAht4AALv/AAAtSwAAiOEAAI3sAACV/wAAmP8AAJv/AAABAQAAnv8AAHrHAABlpwAA//8AAND/AACh/wAAiOIAAC9PAABqsgAApP8AAEVkAABJegAAp/8AADFSAACq/wAAGSoAAAECAACt/wAAM1UAAIjjAAA+aAAAfcgAALb/AAAdMAAAT4MAADpjAABAawAAuf8AAAsQAAC//wAAAAYAAML/AAADBgAACQ4AAMX/AACW/wAAmf8AADdcAACc/wAAn/8AABAcAABRhwAAov8AAIXdAADX/wAAqP8AAKv/AACQ8AAAWI0AAC5NAACx/wAAIEMAALT/AACF3gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISAxISEhISCIiSEgMIkhICkpWLSBIIUg1MEcgEDUiCk4ZCAQUSCQiMTwUNQ0xHxBSTwAvQ0sQSgAzWBw4AFkyUSoABjpOPykANihQCwA3UVEHAjRCRVQyD0wuETs0B1FRNyNVGAkAJh0CGkASAgdRUR4ALCdOAD0BVUFGTQA3UVIbAEQTAAA5AA4/Az4AWTJOUxcFV0klNSs8FDUNMR8QCkoWFhZYNSEVMEcgEDUiCkhIDAwMSEhIIiJISAwiSEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
// @namespace github.com/hmjz100
// @version 1.0.0
// @description 《也许同类型中最好用?》系列 - Pornhub 视频播放器替换 Artplayer,无视频广告,支持进度条热力图,显示视频多分辨率 m3u8/mp4 下载链接,可直接下载 免费/付费/禁下 的视频,并且过滤页面 iframe 广告。
// @author hmjz100
// @license AGPL-3.0-or-later
// @match *://*.pornhub.org/*
// @match *://*.pornhub.com/*
// @match *://*.pornhubpremium.com/*
// @grant unsafeWindow
// @grant GM_setClipboard
// @grant GM_openInTab
// @grant GM_xmlHttpRequest
// @grant GM.xmlHttpRequest
// @connect pornhub.org
// @connect pornhub.com
// @connect pornhubpremium.com
// @connect phncdn.com
// @require https://unpkg.com/[email protected]/dist/jquery.min.js
// @require https://unpkg.com/[email protected]/dist/hls.min.js
// @require https://unpkg.com/[email protected]/dist/artplayer.js
// @run-at document-start
// ==/UserScript==
(function PornhubHelper() {
// 严格模式,确保代码安全执行
'use strict';
// unsafeWindow 检测
if (typeof unsafeWindow === 'undefined') {
window.unsafeWindow = window;
}
/*
防止代码因其他原因被执行多次
代码出自 “Via 轻插件”,作者谷花泰
*/
const key = encodeURIComponent('Pornhub 助手:主代码');
if (window[key]) return;
window[key] = true;
let temp = {
doc: $(document),
ins: 0,
maxQuality: null,
qualityMap: new Map(),
promises: []
}
let base = {
/**
* 可跨域 xmlhttpRequest 请求
* @author hmjz100
* @description 封装 `GreaseMonkey-Compatible_xmlhttpRequest` 实现的跨域请求,与原始函数参数相同
* @param {Object} option - 请求配置对象
* @returns {XMLHttpRequest} 请求对象实例
*/
xmlHttpRequest(option) {
let request = (typeof GM_xmlhttpRequest !== "undefined") ? GM_xmlhttpRequest : GM.xmlHttpRequest;
if (request && typeof request === 'function') {
return request(option);
}
},
/**
* 发送 GET 请求
* @author 油小猴
* @author hmjz100
* @description 支持进度监控、文件下载和自动重试,可处理被下载工具捕获特殊逻辑
* @param {string} url - 请求地址
* @param {Object} headers - 请求头配置
* @param {string} [type='json'] - 响应类型
* @param {Object} [extra] - 附加参数(需包含 `filename` 和 `index` 属性)
* @returns {Promise} 包含响应数据的 `Promise` 对象
*/
get(url, headers, type, extra) {
let newHeaders = {};
for (let key in headers) {
newHeaders[key.toLowerCase().split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join('-')] = headers[key];
}
headers = { "User-Agent": navigator.userAgent, "Origin": location.origin, "Referer": `${location.origin}/`, "Cookie": document.cookie, "DNT": "1", ...newHeaders };
return new Promise((resolve, reject) => {
let sendRequest = () => {
let requestObj = base.xmlHttpRequest({
method: "GET", url, headers,
responseType: type || 'json',
onload: function (res) {
if (res.status === 204) {
console.log('【Pornhub 助手】Get(load)\n\x1B[31m该请求已被某个下载工具捕获。' + (res.statusText ? ("\n\x1B[0m工具提示:\x1B[31m" + res.statusText) : "") + '\x1B[0m\n请求地址:' + url + '\n请求头部:', headers, '\n请求结果:', res);
requestObj.abort();
temp.idm[extra.index] = true;
return;
}
if (type === 'blob') {
console.log('【Pornhub 助手】Get(load) Blob\n请求地址:' + url, '\n请求结果:', res);
res.status === 200 && base.blobDownload(res.response, extra.filename);
resolve(res);
} else {
console.log('【Pornhub 助手】Get(load)\n请求地址:' + url + '\n请求头部:', headers, '\n请求结果:', res);
resolve(res.response || res.responseText);
}
},
onprogress: function (res) {
if (res.status === 204) {
console.log('【Pornhub 助手】Get(progress)\n\x1B[31m该请求已被某个下载工具捕获。' + (res.statusText ? ("\n\x1B[0m工具提示:\x1B[31m" + res.statusText) : "") + '\x1B[0m\n请求地址:' + url + '\n请求头部:', headers, '\n请求结果:', res);
requestObj.abort();
temp.idm[extra.index] = true;
return;
}
if (extra && extra.filename && extra.index) {
res.total > 0 ? temp.progress[extra.index] = (res.loaded * 100 / res.total) : temp.progress[extra.index] = 0.00;
}
},
onloadstart(res) {
if (res.status === 204) {
console.log('【Pornhub 助手】Get(start)\n\x1B[31m该请求已被某个下载工具捕获。' + (res.statusText ? ("\n\x1B[0m工具提示:\x1B[31m" + res.statusText) : "") + '\x1B[0m\n请求地址:' + url + '\n请求头部:', headers, '\n请求结果:', res);
requestObj.abort();
temp.idm[extra.index] = true;
return;
}
console.log('【Pornhub 助手】Get(start)\n请求地址:' + url + '\n请求头部:', headers);
if (extra && extra.filename && extra.index) temp.request[extra.index] = requestObj;
},
onerror: async function (err) {
console.error('【Pornhub 助手】Get(error)\n请求出现错误,可能是网络问题。', err);
reject(err);
},
});
};
sendRequest(); // 初始请求
});
},
/**
* 等待指定元素加载完成并执行回调
* @author hmjz100
* @description 监听 DOM 元素是否出现,若未出现则每隔一段时间重试,直到找到为止。
* 支持在 iframe 内部查找元素,适用于异步加载场景。
* @param {string} selectorElem - 要等待的目标元素选择器
* @param {Function} actionFunction - 找到元素后执行的回调函数,接收 jQuery 元素作为参数,返回 true 可以不再继续寻找
* @param {boolean} [bWaitOnce=false] - 是否只执行一次回调,默认为 false,如果不设置为 true 的话需要自行判断是否对元素进行操作
* @param {string} [iframeSelector] - 若目标元素位于 iframe 中,传入 iframe 的选择器
* @param {string} [controlKey] - 控制唯一性的键名,用于避免重复处理
*/
waitForKeyElements(selectorElem, actionFunction, bWaitOnce, iframeSelector, controlKey) {
// 初始化管理器
const manager = this.waitForKeyElements.manager || (
this.waitForKeyElements.manager = {
observers: new WeakMap(),
tasks: new Map(),
instanceCounter: 0
}
);
const targetDoc = iframeSelector
? $(iframeSelector).get(0)?.contentDocument
: document;
if (!targetDoc) return; // 无效文档直接返回
// 生成唯一控制键
controlKey = controlKey || `wkfe_${manager.instanceCounter++}`;
// 清理重复任务
const existingTask = manager.tasks.get(controlKey);
if (existingTask) {
existingTask.observer.disconnect();
manager.tasks.delete(controlKey);
}
// 创建MutationObserver回调
const processElements = () => {
const elements = $(selectorElem, targetDoc);
let foundActive = false;
elements.each((i, el) => {
const jEl = $(el);
const isProcessed = jEl.data(controlKey);
if (isProcessed) return true; // 跳过已处理元素
const cancelAction = actionFunction(jEl);
if (cancelAction) {
foundActive = true;
} else if (bWaitOnce) {
jEl.data(controlKey, true); // 标记已处理
}
});
// 一次性任务且找到有效元素时清理
if (bWaitOnce && foundActive) {
observer.disconnect();
manager.tasks.delete(controlKey);
}
};
// 创建Observer实例
const observer = new MutationObserver(processElements);
// 配置并启动观察
observer.observe(targetDoc.documentElement, {
childList: true,
subtree: true,
attributes: true,
characterData: true
});
// 注册任务
manager.tasks.set(controlKey, {
observer,
targetDoc
});
// 立即执行初始检查
processElements();
},
preparePlayer(flashvars) {
let video = {
id: new URL(flashvars.link_url).searchParams.get("viewkey"),
title: flashvars.video_title,
url: null,
qualitys: {},
title: flashvars.video_title,
cover: flashvars.image_url,
hotspots: flashvars.hotspots
}
flashvars.mediaDefinitions.forEach(item => {
let format = item.format;
let quality = Number(item.quality);
let url = new URL(item.videoUrl);
let hasMap = temp.qualityMap.has(quality);
if (format === 'hls' && quality > 0 && !hasMap) temp.qualityMap.set(quality, { mp4: null, hls: url.href })
else if (format === 'hls' && quality > 0 && hasMap) temp.qualityMap.get(quality).hls = url.href;
if (format === 'mp4') {
url.host = location.host;
let promise = base.get(url.href).then(res => {
res.forEach(item => {
let format = item.format;
let quality = Number(item.quality);
let url = new URL(item.videoUrl);
//url.host = 'dv1.phncdn.com';
let hasMap = temp.qualityMap.has(quality);
if (format === 'mp4' && quality > 0 && !hasMap) temp.qualityMap.set(quality, { mp4: url.href, hls: null })
else if (format === 'mp4' && quality > 0 && hasMap) temp.qualityMap.get(quality).mp4 = url.href;
});
});
temp.promises.push(promise);
}
});
[...temp.qualityMap.keys()].sort((a, b) => b - a).forEach(quality => {
video.qualitys[quality] = temp.qualityMap.get(quality);
// 更新最高清晰度
if (quality > temp.maxQuality) {
temp.maxQuality = quality;
video.url = video.qualitys[quality].hls || video.qualitys[quality].mp4;
}
});
console.log(`【Pornhub 助手】Info\n视频信息:\n`, video, Object.keys(video.qualitys).map(Number).sort((a, b) => b - a).filter(q => video.qualitys[q].hls).map(q => ({ url: video.qualitys[q].hls, html: base.getQualityName(q), quality: q, default: q === temp.maxQuality })));
base.waitForKeyElements("div.video-wrapper div#player", function (element) {
let player = $('<div class="index-module_art-player" style="overflow: hidden; aspect-ratio: 16/9; border-radius: 10px;"></div>')
element.after(player);
element.fadeOut();
base.artPlay(video.id, video.url, Object.keys(video.qualitys).map(Number).sort((a, b) => b - a).filter(q => video.qualitys[q].hls).map(q => ({ url: video.qualitys[q].hls, html: base.getQualityName(q), quality: q, default: q === temp.maxQuality })), video.cover, (art) => {
art.controls.add({
name: 'hideList',
index: 50,
position: 'right',
html: '<i class="art-icon"><svg width="18" height="18" viewBox="0 0 1152 1024"><path fill="#fff" d="M1075.2 0H76.8A76.8 76.8 0 0 0 0 76.8v870.4a76.8 76.8 0 0 0 76.8 76.8h998.4a76.8 76.8 0 0 0 76.8-76.8V76.8A76.8 76.8 0 0 0 1075.2 0zM1024 128v768H128V128h896zm-576"></path></svg></i>',
tooltip: '宽屏模式',
click: function () {
if ($('#vpContentContainer .topSectionGrid').attr("style")) {
$('#vpContentContainer .topSectionGrid').removeAttr("style");
$('#vpContentContainer .sideColumn').removeAttr("style");
$('.container.videoPageGrid').removeAttr("style");
} else {
$('#vpContentContainer .topSectionGrid').css({ "display": "block" });
$('#vpContentContainer .sideColumn').css({ "display": "none" });
$('.wrapper .container').css({ "max-width": "80%" });
}
}
})
art.on('fullscreen', () => {
$('header').fadeToggle();
$(art.controls.fullscreenWeb).toggle();
$(art.controls.hideList).toggle();
});
art.on('fullscreenWeb', () => {
$(art.controls.fullscreen).toggle();
$(art.controls.hideList).toggle();
});
if (video.hotspots) base.addHeatMap(art, video.hotspots);
});
return true;
}, true)
base.waitForKeyElements("div.playerWrapper", function (element) {
let player = $('<div class="index-module_art-player" style="overflow: hidden; aspect-ratio: 16/9; margin-top: 60px;"></div>')
element.after(player);
element.fadeOut();
base.artPlay(video.id, video.url, Object.keys(video.qualitys).map(Number).sort((a, b) => b - a).filter(q => video.qualitys[q].hls).map(q => ({ url: video.qualitys[q].hls, html: base.getQualityName(q), quality: q, default: q === temp.maxQuality })), video.cover, (art) => {
art.on('fullscreen', () => {
$('header').fadeToggle();
$(art.controls.fullscreenWeb).toggle();
$(art.controls.hideList).toggle();
});
art.on('fullscreenWeb', () => {
$(art.controls.fullscreen).toggle();
$(art.controls.hideList).toggle();
});
if (video.hotspots) base.addHeatMap(art, video.hotspots);
});
}, true)
base.waitForKeyElements("div.video-wrapper div.video-actions-container div.video-actions-tabs div.video-action-tab.about-tab,div.categoryTags div.tooltipWrapper", function (element) {
let downloadContent = $(`<div class="helper"><ul class="links"></ul><li class="tips"><span>小贴士:MP4 总是提示 Unauthorized?复制 HLS 链接到猫抓插件(需单独安装)的 M3U8 解析器里再下载吧~</span></li></div>`);
element.before(downloadContent);
Object.keys(video.qualitys).map(Number).sort((a, b) => b - a).filter(q => video.qualitys[q].hls).map(q => ({ url: video.qualitys[q].hls, html: base.getQualityName(q), quality: q, default: q === temp.maxQuality })).forEach(item => {
let downloadItem = $(`<li>
<span class="name">HLS ${item.html}</span>
<input class="link" value="${item.url}" />
<span class="more"><a class="copy" href="javascript:void(0)" data-copy="${item.url}">复制</a></span>
</li>`);
downloadContent.find("ul").append(downloadItem);
});
Promise.all(temp.promises).then(() => {
Object.keys(video.qualitys).map(Number).sort((a, b) => b - a).filter(q => video.qualitys[q].mp4).map(q => ({ url: video.qualitys[q].mp4, html: base.getQualityName(q), quality: q, default: q === temp.maxQuality })).forEach(item => {
let downloadItem = $(`<li>
<span class="name">MP4 ${item.html}</span>
<input class="link" value="${item.url}" />
<span class="more">
<a class="download" href="${item.url}" target="_blank">直链</a>
<a class="copy" href="javascript:void(0)" data-copy="${item.url}">复制</a>
</span>
</li>`);
downloadContent.find("ul").append(downloadItem);
});
})
}, true);
},
artPlay(id, url, urls, cover, actionFunction) {
let options = {
id: id,
container: '.index-module_art-player',
volume: 1,
autoPlayback: true,
theme: '#ff9000',
customType: {
m3u8: function (video, url, art) {
if (Hls.isSupported()) {
if (art.hls) art.hls.destroy();
let hls = new Hls();
hls.loadSource(url);
hls.attachMedia(video);
art.hls = hls;
art.on('destroy', () => hls.destroy());
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = url;
} else {
art.notice.show = '不支持的格式: m3u8';
}
},
},
controls: [
{
name: 'star',
index: 21,
position: 'right',
html: '<i class="art-icon"><svg width="18" height="18" viewBox="0 0 576 512"><path d="M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z"></path></svg></i>',
tooltip: '来点个 Star ?',
click: function () {
GM_openInTab('https://github.com/hmjz100/PornhubHelper', { active: true });
},
}
],
icons: {
state: '<svg width="100" height="100" fill="none"><g opacity=".9"><path opacity=".5" d="M50 4.167C24.687 4.167 4.167 24.687 4.167 50c0 25.314 20.52 45.834 45.833 45.834 25.314 0 45.834-20.52 45.834-45.834 0-25.313-20.52-45.833-45.834-45.833Z" fill="#000"/><path d="M69.194 53.043 43.153 70.231a3.646 3.646 0 0 1-5.653-3.043V32.816a3.646 3.646 0 0 1 5.654-3.043l26.042 17.188c2.183 1.44 2.183 4.645 0 6.086-2.184 1.44 6.51-4.3-.002-.004Z" fill="#fff"/></g></svg>',
indicator: '<img width="16" heigth="16" style="border-radius:50px" src="data:image/x-icon;base64,AAABAAEAEBAAAAEACABoBQAAFgAAACgAAAAQAAAAIAAAAAEACAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAAAAAAAAAAABorAAACAwAAwP8AAG+5AAA0VgAAUYkAAInkAAA/aQAAMFEAAJf/AABZlAAAmv8AAGSnAAAaLAAAERwAAKP/AABHZAAABAcAAC0xAACp/wAArP8AALL/AABDcAAAbowAAI/uAAAKEAAAht4AALv/AAAtSwAAiOEAAI3sAACV/wAAmP8AAJv/AAABAQAAnv8AAHrHAABlpwAA//8AAND/AACh/wAAiOIAAC9PAABqsgAApP8AAEVkAABJegAAp/8AADFSAACq/wAAGSoAAAECAACt/wAAM1UAAIjjAAA+aAAAfcgAALb/AAAdMAAAT4MAADpjAABAawAAuf8AAAsQAAC//wAAAAYAAML/AAADBgAACQ4AAMX/AACW/wAAmf8AADdcAACc/wAAn/8AABAcAABRhwAAov8AAIXdAADX/wAAqP8AAKv/AACQ8AAAWI0AAC5NAACx/wAAIEMAALT/AACF3gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISAxISEhISCIiSEgMIkhICkpWLSBIIUg1MEcgEDUiCk4ZCAQUSCQiMTwUNQ0xHxBSTwAvQ0sQSgAzWBw4AFkyUSoABjpOPykANihQCwA3UVEHAjRCRVQyD0wuETs0B1FRNyNVGAkAJh0CGkASAgdRUR4ALCdOAD0BVUFGTQA3UVIbAEQTAAA5AA4/Az4AWTJOUxcFV0klNSs8FDUNMR8QCkoWFhZYNSEVMEcgEDUiCkhIDAwMSEhIIiJISAwiSEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=">',
play: '<svg width="13" height="13" viewBox="0 0 87.5 100"><path d="m0 0v100l87.5-50z"></path></svg>',
pause: '<svg width="13" height="13" viewBox="0 0 81.75 100"><path d="m56.596 100v-100h25.154v100zm-56.596-100h25.154v100h-25.154z"></path></svg>',
volume: '<svg width="18" height="18" viewBox="0 0 120 100" fill="none"><path d="m30 25 39.999-25v100l-39.999-25zm-30 50h20v-50h-20zm101.32-66.32-7.4851 7.485c9.2351 8.915 14.915 20.785 14.915 33.835s-5.6752 24.92-14.915 33.83l7.4899 7.49c11.525-10.765 18.676-25.275 18.676-41.32 0-16.046-7.1498-30.55-18.68-41.32zm-2.4453 41.32c0-10.42-4.5747-19.835-11.919-26.954l-7.5146 7.515c5.0547 5.245 8.1845 12.005 8.1845 19.44 0 7.44-3.1146 14.205-8.1697 19.456l7.515 7.5147c7.34-7.12 11.904-16.544 11.904-26.97z" stroke-width="1.1995"></path></svg>',
volumeClose: '<svg width="18" height="18" viewBox="0 0 120 100" fill="none"><path d="m83.949 30.656v65.176l-42.579-20.834v-3.2712zm7.9837-25.393-7.1156-5.2669-15.84 17.697-27.608 15.637v15.192l-9.1933 10.267v-25.459h-18.387v41.668h3.8749l-17.664 19.734 7.1156 5.267z" stroke-width="1.0504"></path></svg>',
setting: '<svg width="18" height="18" viewBox="0 0 97.3 100"><path d="m85.822 54.9c0.19964-1.6004 0.35-3.2002 0.35-4.9002s-0.15036-3.3-0.35-4.9l10.553-8.2497c0.95018-0.75 1.2004-2.1 0.60018-3.2l-10.003-17.3c-0.60018-1.1-1.9509-1.5-3.0515-1.1l-12.453 5c-2.601-2-5.4016-3.65-8.4525-4.9l-1.901-13.25c-0.14973-1.2-1.2004-2.1-2.4506-2.1h-20.006c-1.2504 0-2.3007 0.9-2.4508 2.1l-1.9006 13.25c-3.051 1.25-5.8518 2.95-8.4525 4.9l-12.453-5c-1.1503-0.45-2.4508 0-3.051 1.1l-10.003 17.3c-0.65022 1.1-0.35013 2.45 0.60018 3.2l10.553 8.2497c-0.20008 1.6-0.35013 3.25-0.35013 4.9 0 1.65 0.15005 3.2998 0.35013 4.9002l-10.553 8.2497c-0.95031 0.75032-1.2004 2.1-0.60018 3.2002l10.003 17.3c0.60018 1.0996 1.9506 1.5 3.051 1.0996l12.453-4.9996c2.6008 1.9996 5.4016 3.6499 8.4525 4.8998l1.9006 13.25c0.15004 1.2 1.2004 2.1 2.4508 2.1h20.006c1.2503 0 2.3009-0.9 2.4506-2.1l1.901-13.25c3.0508-1.2499 5.8515-2.9501 8.4525-4.8998l12.453 4.9996c1.1505 0.45032 2.4513 0 3.0515-1.0996l10.003-17.3c0.60018-1.1002 0.35-2.4499-0.60018-3.2002zm-37.161 12.6c-9.6528 0-17.505-7.8499-17.505-17.5 0-9.6499 7.8523-17.5 17.505-17.5 9.6528 0 17.505 7.8499 17.505 17.5 0 9.6506-7.8523 17.5-17.505 17.5z"></path></svg>',
},
setting: true,
hotkey: true,
flip: true,
playbackRate: true,
aspectRatio: true,
miniProgressBar: true,
fullscreen: true,
fullscreenWeb: true,
fastForward: true,
autoOrientation: true,
lock: true,
moreVideoAttr: {
'preload': 'none'
},
}
if (cover) options.poster = cover;
if (url) options.url = url;
if (urls) options.plugins = [artplayPluginQuality(urls)];
function artplayPluginQuality(option) {
return art => {
let storageQuality = art.storage.get('quality');
if (storageQuality) {
let quality = option.find(item => item.html === storageQuality);
if (quality) {
option.forEach(item => delete item.default);
quality.default = true;
} else if (!option.find(item => item.default)) {
option[0].default = true;
}
}
art.controls.add({
name: 'quality',
position: 'right',
html: option.find(item => item.default).html,
selector: option,
style: { "margin-right": '10px' },
onSelect: function (item) {
art.switchQuality(item.url, item.html);
art.storage.set('quality', item.html);
return item.html
},
});
if (storageQuality) {
let quality = option.find(item => item.html === storageQuality);
if (quality) {
art.url = quality.url;
} else {
art.url = option[0].url;
}
} else {
art.url = option[0].url;
}
}
}
var art = new Artplayer(options)
art.on('fullscreen', () => {
$('header').toggle();
$(art.controls.goodRing).toggle();
if (!$('body').hasClass('no-scroll')) {
$('body').addClass('no-scroll');
} else {
$('body').removeClass('no-scroll');
}
});
art.on('fullscreenWeb', () => {
$('header').toggle();
$(art.controls.goodRing).toggle();
if (!$('body').hasClass('no-scroll')) {
$('body').addClass('no-scroll');
} else {
$('body').removeClass('no-scroll');
}
});
var contextmenuStyle = {
"display": "flex",
"justify-content": "center",
"align-items": "center",
"border-bottom": "none"
}
art.contextmenu.add({
name: 'appTitle',
index: 1,
html: `<img width="16" heigth="16" src="data:image/x-icon;base64,AAABAAEAEBAAAAEACABoBQAAFgAAACgAAAAQAAAAIAAAAAEACAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAAAAAAAAAAABorAAACAwAAwP8AAG+5AAA0VgAAUYkAAInkAAA/aQAAMFEAAJf/AABZlAAAmv8AAGSnAAAaLAAAERwAAKP/AABHZAAABAcAAC0xAACp/wAArP8AALL/AABDcAAAbowAAI/uAAAKEAAAht4AALv/AAAtSwAAiOEAAI3sAACV/wAAmP8AAJv/AAABAQAAnv8AAHrHAABlpwAA//8AAND/AACh/wAAiOIAAC9PAABqsgAApP8AAEVkAABJegAAp/8AADFSAACq/wAAGSoAAAECAACt/wAAM1UAAIjjAAA+aAAAfcgAALb/AAAdMAAAT4MAADpjAABAawAAuf8AAAsQAAC//wAAAAYAAML/AAADBgAACQ4AAMX/AACW/wAAmf8AADdcAACc/wAAn/8AABAcAABRhwAAov8AAIXdAADX/wAAqP8AAKv/AACQ8AAAWI0AAC5NAACx/wAAIEMAALT/AACF3gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISAxISEhISCIiSEgMIkhICkpWLSBIIUg1MEcgEDUiCk4ZCAQUSCQiMTwUNQ0xHxBSTwAvQ0sQSgAzWBw4AFkyUSoABjpOPykANihQCwA3UVEHAjRCRVQyD0wuETs0B1FRNyNVGAkAJh0CGkASAgdRUR4ALCdOAD0BVUFGTQA3UVIbAEQTAAA5AA4/Az4AWTJOUxcFV0klNSs8FDUNMR8QCkoWFhZYNSEVMEcgEDUiCkhIDAwMSEhIIiJISAwiSEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=">Pornhub 增强`,
style: contextmenuStyle,
click: function () {
GM_openInTab('https://github.com/hmjz100/PornhubHelper', { active: true });
},
})
art.contextmenu.update({
name: 'version',
index: 2,
html: `<img width="15" heigth="15" src="https://artplayer.org/assets/img/logo.png"/>Artplayer Ultra ${Artplayer.version}`,
click: function () {
GM_openInTab('https://artplayer.org/', { active: true });
},
style: contextmenuStyle
})
art.contextmenu.update({
name: 'info',
index: 40,
html: `${art.i18n.language["Video Info"]}`,
style: contextmenuStyle
})
art.contextmenu.update({
name: 'close',
index: 50,
html: `${art.i18n.language["Close"]}`,
style: contextmenuStyle
})
unsafeWindow.art = art
$(art.template.$container).find(".icon").removeClass("icon")
actionFunction ? actionFunction(art) : ""
},
addHeatMap(art, hotspots) {
function smoothData(data, windowSize) {
var smoothed = [];
for (let i = 0; i < data.length; i++) {
let sum = 0;
let count = 0;
for (let j = Math.max(0, i - windowSize); j <= Math.min(data.length - 1, i + windowSize); j++) {
sum += data[j];
count++;
}
smoothed.push(sum / count);
}
return smoothed;
}
// 应用平滑处理
hotspots = smoothData(hotspots.slice(5), 2);
// 使用新版本的控件系统实现随进度条效果
const { query } = art.constructor.utils;
art.controls.add({
name: 'heatmap',
position: 'top',
html: '',
style: {
position: 'absolute',
top: '-30px',
left: '0px',
right: '0px',
height: '30px',
width: '100%',
pointerEvents: 'none',
},
mounted($heatmap) {
let $start = null;
let $stop = null;
function update() {
$heatmap.innerHTML = '';
if (!art.duration || art.option.isLive) return;
const svg = {
w: $heatmap.offsetWidth,
h: $heatmap.offsetHeight,
};
const options = {
opacity: 0.2,
minHeight: Math.floor(svg.h * 0.05),
};
// 使用点计算逻辑(确保平滑)
let points = [];
const svgWidth = svg.w;
const svgHeight = svg.h;
const max = Math.max(...hotspots);
// 添加左下角作为起点
points.push([0, svgHeight]);
// 添加数据点
hotspots.forEach((value, index) => {
const x = (index / (hotspots.length - 1)) * svgWidth;
const y = svgHeight - (value / max) * svgHeight;
points.push([x, y]);
});
// 添加右下角作为终点
points.push([svgWidth, svgHeight]);
// 闭合路径
points.push([0, svgHeight]);
// 生成路径字符串
let pathD = '';
points.forEach((point, index) => {
if (index === 0) {
pathD += `M ${point[0]},${point[1]} `;
} else {
pathD += `L ${point[0]},${point[1]} `;
}
});
pathD += 'Z';
// 创建SVG
$heatmap.innerHTML = `<svg viewBox="0 0 ${svg.w} ${svg.h}" preserveAspectRatio="none">
<defs>
<linearGradient id="heatmap-solids" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:var(--art-theme);stop-opacity:${options.opacity}" />
<stop offset="0%" style="stop-color:var(--art-theme);stop-opacity:${options.opacity}" id="heatmap-start" />
<stop offset="0%" style="stop-color:var(--art-progress-color);stop-opacity:1" id="heatmap-stop" />
<stop offset="100%" style="stop-color:var(--art-progress-color);stop-opacity:1" />
</linearGradient>
</defs>
<path fill="url(#heatmap-solids)" d="${pathD}"></path>
</svg>`;
$start = query('#heatmap-start', $heatmap);
$stop = query('#heatmap-stop', $heatmap);
if ($start && $stop) {
$start.setAttribute('offset', `${art.played * 100}%`);
$stop.setAttribute('offset', `${art.played * 100}%`);
}
}
// 更新渐变位置
art.on('video:timeupdate', () => {
if ($start && $stop) {
$start.setAttribute('offset', `${art.played * 100}%`);
$stop.setAttribute('offset', `${art.played * 100}%`);
}
});
art.on('setBar', (type, percentage) => {
if ($start && $stop && type === 'played') {
$start.setAttribute('offset', `${percentage * 100}%`);
$stop.setAttribute('offset', `${percentage * 100}%`);
}
});
// 初始化和响应式更新
art.on('ready', () => update());
art.on('resize', () => update());
art.on('setBar', () => update());
update();
},
});
},
/**
* 根据垂直分辨率返回视频质量的标准命名
* @param {number} quality 视频的垂直分辨率(像素高度)
* @returns {string} 标准分辨率命名
*/
getQualityName(quality) {
if (typeof (quality) !== "number") quality = Number(quality);
if (quality <= 144) return '144P 低清';
if (quality <= 240) return '240P 低清';
if (quality <= 360) return '360P 低清'; // 低清 - Low Definition
if (quality <= 480) return '480P 标清'; // 标清 - Standard Definition
if (quality <= 540) return '540P 半高清'; // 半高清 - Extended Definition
if (quality <= 720) return '720P 高清'; // 高清 - High Definition
if (quality <= 1080) return '1080P 全高清'; // 全高清 - Full High Definition
if (quality < 1440) return '~2K 高清';
if (quality === 1440) return '2K 高清'; // 2K 高清 - Quad High Definition
if (quality < 1600) return '~2.5K 高清';
if (quality === 1600) return '2.5K 高清';
if (quality < 2160) return '~4K 超高清';
if (quality === 2160) return '4K 超高清'; // 超高清 - Ultra High Definition
if (quality < 4320) return '~8K 超高清';
if (quality === 4320) return '8K 超高清'; // 8K 超高清 - 8K Ultra High Definition
return `${Math.round(quality / 1000)}K 超高清`;
},
greenPage() {
// 页面样式
base.waitForKeyElements("head", (element) => {
element.after(`<style>
a, button, ul li, ui, label, input, select {
transition: all 0.25s !important;
-webkit-transition: all 0.25s !important;
}
.art-contextmenu svg, .art-contextmenu img {
vertical-align: top;
margin-right: 5px
}
.no-scroll {
overflow: hidden !important;
}
div.helper {
width: 100%;
margin: 0 0 3%;
}
div.helper > ul.links > li {
display: flex;
gap: 1%;
margin: 0 0 5px;
}
div.helper > ul.links > li > .link {
border: 1px solid #767676;
flex: 1;
}
/* Pornhub Modern Design */ html,body{background:#0e0e0e!important}.tab-menu-item{display:flex;align-items:center}.tab-menu-item{display:flex;align-items:center}.tab-menu-wrapper-cell:nth-child(n+2){background:red;display:none!important}.userActions{margin:0 0 0 16px!important}.userActions>.subscribeButton>button{min-width:0!important;line-height:40px!important;padding:0 16px!important;border:none!important;background-color:#ff9000!important;color:#fff!important;border-radius:32px!important;margin:0!important;font-weight:400!important}.userActions>.videoFanClubButton a{min-width:0!important;line-height:40px!important;padding:0 16px!important;border:none!important;background-color:#6b3d75!important;color:#fff!important;border-radius:32px!important;margin:0!important;font-weight:400!important}button>i.ph-icon-rss-feed{display:none}button>.buttonLabel{margin:0!important}.video-info-row.userRow{display:flex!important}.userAvatar>img{width:40px!important;height:40px!important}.categoriesWrapper>a.item{border-radius:32px!important}.title>span{font-weight:600;font-size:16px!important}#topRightProfileMenu{width:auto!important;display:flex!important}#headerSignupLink{display:none!important}#headerLoginLink{padding:8px 16px!important;border-radius:32px;border:1px solid rgba(255,255,255,.1)}#headerWrapper{background-color:#0f0f0f!important}.logoWrapper>a{padding:16px 14px 16px 16px}.logo img{min-height:auto!important;max-height:none!important;height:24px!important;width:auto}#searchBarContainer{height:32px!important;align-items:center;border-radius:48px!important}#searchInput{padding:0!important;border-radius:48px!important}.headerBtnsWrapper{display:none!important}#headerSearchWrapperFree{flex:0 1 578px}#searchBarContainer{padding:0 8px}#searchesWrapper{position:absolute;top:48px;margin:0!important}#header{grid-template-rows:56px!important}#headerContainer{display:flex!important;justify-content:space-between}#recommendedVideosVPage>.section_bar_sidebar{display:none}#hd-rightColVideoPage>div:not(#recommendedVideosVPage){display:none!important}.more_recommended_btn{border-radius:48px!important;width:100%;padding:8px 0}.phimage{border-radius:10px}.sectionWrapper{margin:20px 0}.rating-container{display:none}.cookiesBanner{display:none}#header{position:sticky;top:0}.networkBarWrapper{display:none}.title-container,.video-actions-container,.video-wrapper{background:0 0!important}.nestedBlock{padding-left:48px!important}#hd-leftColVideoPage{display:flex;flex-direction:column}#hd-leftColVideoPage>.video-wrapper{order:1}#hd-leftColVideoPage>#under-player-comments{order:2}#hd-leftColVideoPage>.relatedVideos{order:3}#hd-leftColVideoPage>#under-player-playlists{order:4}.title-container{padding:8px 0!important}.video-actions-menu{margin:0!important}.video-actions-container{padding:16px 0!important}.ratingInfo{display:flex;gap:8px;align-items:center}.views{padding-left:0!important}.commentLogMessage{display:none!important}.cmtHeader{gap:16px;padding:0!important}#cmtContent{padding:0!important}#cmtWrapper .taggablePlaceholder{border-radius:0 10px 10px!important;-moz-border-radius:0 10px 10px!important;-webkit-border-radius:0 10px 10px!important;-ms-border-radius:0 10px 10px!important;-o-border-radius:0 10px 10px!important}.abovePlayer,.favorites-message{display:none}.video-actions-menu>.reset{display:none}.video-actions-menu{display:flex;justify-content:space-between;align-items:center}#hd-rightColVideoPage.wide{margin-top:748px!important}
</style>`);
}, true)
// 空白间距
base.waitForKeyElements("li.emptyBlockSpace", (tag) => {
if (tag.attr('removed')) return;
tag.attr('removed', true);
tag.fadeOut()
}, true)
// 顶栏广告
base.waitForKeyElements("#headerUpgradePremiumBtn", (tag) => {
if (tag.parent().attr('removed')) return;
tag.parent().attr('removed', true);
tag.parent().fadeOut()
}, true)
// 年龄认证
base.waitForKeyElements('[id^="age-verification"]', (tag) => {
if (tag.attr('removed')) return;
tag.attr('removed', true);
tag.fadeOut()
}, true)
// 页面 iframe 广告
base.waitForKeyElements('iframe[allowtransparency][data-embeddedads][data-spot-id], ins[class*="adsby"]', (tag) => {
function findParentWithClass(el) {
if (el.length > 0) {
if (el.parent().attr("class")) {
if (el.parent().attr('removed') || el.parent().parent().parent().attr('removed')) return;
if (el.parent().parent().parent().prop('tagName') === "LI") {
el.parent().parent().parent().attr('removed', true);
console.log(`【Pornhub 助手】AD\n`, el.parent().parent().parent().attr("class"), "有class且有li,隐藏!")
el.parent().parent().parent().fadeOut();
} else {
if (el.parent().attr("id") && el.parent().attr("id").includes("playerDiv_")) return;
el.parent().attr('removed', true);
console.log(`【Pornhub 助手】AD\n`, el.parent().attr("class"), "有class,隐藏!")
el.parent().fadeOut();
}
} else {
console.log(`【Pornhub 助手】AD\n`, "没有class,继续查找...")
findParentWithClass(el.parent());
}
}
}
findParentWithClass(tag);
}, true)
// 视频卡片广告
base.waitForKeyElements('.bg-spice-badge', (tag) => {
function findParentWithClass(el) {
if (el.length > 0) {
if (el.parent().prop('tagName') === "LI") {
if (el.parent().attr('removed')) return;
el.parent().attr('removed', true);
console.log(`【Pornhub 助手】AD\n`, el.parent().attr("class"), "有li,隐藏!")
el.parent().fadeOut();
} else {
console.log(`【Pornhub 助手】AD\n`, "没有class,继续查找...")
findParentWithClass(el.parent());
}
}
}
findParentWithClass(tag);
}, true)
// 广告被屏蔽提示
base.waitForKeyElements("body div.bottomNav, div#js-abContainterMain", (tag) => {
tag.fadeOut()
}, true)
// 暂停原始播放器的播放,避免替换后没法暂停
base.waitForKeyElements("div.mgp_videoWrapper video", function (video) {
let ins = 0;
let waitPause = setInterval(() => {
if (video[0].readyState === 0) return;
video[0].pause();
if (ins++ >= 1000) clearInterval(waitPause);
}, 100)
}, true)
temp.doc.on('click', '.helper .copy', function (e) {
e.preventDefault();
let element = $(this);
GM_setClipboard(element.data('copy'));
element.text("成功");
setTimeout(() => element.text("复制"), 1000);
});
}
}
base.greenPage();
let waitArt = setInterval(() => {
if (typeof (Artplayer) === "undefined") return;
Artplayer.LOG_VERSION = false;
Artplayer.PLAYBACK_RATE = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3];
if (temp.ins++ >= 100) clearInterval(waitArt);
}, 1)
let waitForFlashvars = setInterval(async () => {
let flashvars = {};
for (let prop in unsafeWindow) {
if (prop.startsWith('flashvars_')) {
flashvars = unsafeWindow[prop];
clearInterval(waitForFlashvars);
} else continue;
}
if (!flashvars.length && !flashvars.mediaDefinitions) return;
console.log(`【Pornhub 助手】Info\n页面信息:\n`, flashvars);
base.preparePlayer(flashvars);
}, 1)
})();