// ==UserScript==
// @name XVideos-优化
// @namespace https://sleazyfork.org/zh-CN/users/1461640-星宿老魔
// @author 星宿老魔
// @license MIT
// @version 1.0.0
// @description 近期标签记忆、已观看隐藏、自动播放-宽屏-高画质-等
// @match *://*.xvideos.com/*
// @icon https://www.xvideos.com/favicon-32x32.png
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_notification
// @run-at document-start
// ==/UserScript==
// @license MIT
(function(){"use strict";const c={features:{hideWatched:!0},storageKeys:{hideWatched:"xv_hide_watched"},ui:{debounceDelay:150}};class x{static init(){this.addBaseStyles()}static addBaseStyles(){GM_addStyle(`
/* 隐藏广告元素 */
#warning-survey, /* 蓝色警告横幅 */
#video-right, /* 视频右侧广告容器 */
#ad-footer /* 页面底部广告 */ { display: none !important; }
/* 隐藏菜单栏广告链接 */
.head__menu-line__main-menu__lvl1.red-ticket,
.head__menu-line__main-menu__lvl1.live-cams,
.head__menu-line__main-menu__lvl1.nutaku-games,
.head__menu-line__main-menu__lvl1.ignore-popunder {
display: none !important;
}
/* 隐藏包含特定链接的菜单项 */
a[href*="xvideos.red/red/videos"],
a[href*="zlinkt.com"],
a[href*="nutaku.net"] {
display: none !important;
}
/* 隐藏父级菜单项 */
li:has(> a[href*="xvideos.red/red/videos"]),
li:has(> a[href*="zlinkt.com"]),
li:has(> a[href*="nutaku.net"]) {
display: none !important;
}
/* 隐藏红色横幅广告 */
div[style*="background: rgb(222, 38, 0)"],
div[id*="9o6lsm8aj09wav6bf9"] {
display: none !important;
}
/* 隐藏包含xvideos.red推广的元素 */
p:has(> a[href*="xvideos.red?pmsc=header_adblock"]) {
display: none !important;
}
/* 基础动画 */
.xv-fade-in {
animation: xvFadeIn 300ms ease-in-out;
}
@keyframes xvFadeIn {
from { opacity: 0; transform: translateY(-5px); }
to { opacity: 1; transform: translateY(0); }
}
/* 近期标签容器样式 */
#recent-tags-container {
background: #f5f7fa !important;
border-bottom: 1px solid #e5e7eb;
padding: 6px 10px;
line-height: 1;
}
#recent-tags-container .xv-title {
font-weight: 600;
color: #374151;
margin-right: 8px;
}
#recent-tags-container .xv-tag {
display: inline-block;
font-size: 12px;
color: #374151;
text-decoration: none;
background: #ffffff;
border: 1px solid #d1d5db;
border-radius: 999px;
padding: 3px 10px;
margin: 0 3px;
}
#recent-tags-container .xv-tag:hover {
background: #f9fafb;
border-color: #9ca3af;
}
/* 切换开关样式 */
.xv-toggle {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #495057;
user-select: none;
}
.xv-switch {
position: relative;
width: 46px;
height: 24px;
}
.xv-switch input {
opacity: 0;
width: 0;
height: 0;
}
.xv-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #ced4da;
border-radius: 24px;
transition: background 0.3s ease;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
}
.xv-slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 2px;
top: 2px;
background: #ffffff;
border-radius: 50%;
transition: transform 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.xv-switch input:checked + .xv-slider {
background: #28a745;
}
.xv-switch input:checked + .xv-slider:before {
transform: translateX(22px);
}
/* 稍后观看:缩略图右下角移除按钮 */
.xv-watchlater-remove {
position: absolute;
right: 6px;
bottom: 6px;
width: 26px;
height: 26px;
border-radius: 50%;
background: rgba(239,68,68,.92);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-weight: 700;
line-height: 1;
box-shadow: 0 2px 6px rgba(0,0,0,.25);
z-index: 5;
}
.xv-watchlater-remove:hover {
background: rgba(220,38,38,.98);
}
.xv-watchlater-remove .xv-remove-icon {
pointer-events: none;
font-size: 16px;
}
/* 隐藏已观看眼睛图标样式 */
.xv-eye-icon {
transition: all 0.3s ease;
}
/* 悬停效果 - 与原生按钮一致 */
#xv-hide-watched-menu-item .head__menu-line__main-menu__lvl1:hover .xv-eye-icon {
color: #d32f2f !important; /* 悬停时统一显示红色 */
}
#xv-hide-watched-menu-item .head__menu-line__main-menu__lvl1:active .xv-eye-icon {
color: #b71c1c !important; /* 点击时更深的红色 */
}
`)}}const a={select(n,e=document){return e.querySelector(n)},selectAll(n,e=document){return e.querySelectorAll(n)},create(n,e={},t=""){const i=document.createElement(n);return Object.entries(e).forEach(([s,o])=>{s==="className"?i.className=o:s==="style"&&typeof o=="object"?Object.assign(i.style,o):s.startsWith("on")&&typeof o=="function"?i.addEventListener(s.slice(2),o):i.setAttribute(s,o)}),t&&(i.innerHTML=t),i}},l={get(n,e=null){try{const t=typeof GM_getValue<"u"?GM_getValue(n):localStorage.getItem(n);return t!=null?JSON.parse(t):e}catch(t){return console.warn(`[XVideos] 读取存储失败: ${n}`,t),e}},set(n,e){try{const t=JSON.stringify(e);return typeof GM_setValue<"u"?GM_setValue(n,t):localStorage.setItem(n,t),!0}catch(t){return console.warn(`[XVideos] 写入存储失败: ${n}`,t),!1}}};function m(n,e){let t;return function(...i){clearTimeout(t),t=setTimeout(()=>n(...i),e)}}class _{static init(){this.setupStaticAdRemoval(),this.setupDynamicAdRemoval()}static setupStaticAdRemoval(){}static setupDynamicAdRemoval(){document.addEventListener("DOMContentLoaded",()=>{this.removeRedBanner(),this.removeMenuAds(),this.observeAds()})}static removeRedBanner(){const e=a.select('a[href*="xvideos.red?pmsc=header_adblock"]');if(e){const t=e.closest('div[style*="background: rgb(222, 38, 0)"]');t&&!t.dataset.xvRemoved&&(t.remove(),t.dataset.xvRemoved="true",console.log("[XVideos] 已移除广告横幅"))}}static removeMenuAds(){const e=[".head__menu-line__main-menu__lvl1.red-ticket",".head__menu-line__main-menu__lvl1.live-cams",".head__menu-line__main-menu__lvl1.nutaku-games",".head__menu-line__main-menu__lvl1.ignore-popunder",'a[href*="xvideos.red/red/videos"]','a[href*="zlinkt.com"]','a[href*="nutaku.net"]'];let t=0;e.forEach(s=>{a.selectAll(s).forEach(r=>{if(r&&!r.dataset.xvRemoved){const f=r.closest("li")||r;f.dataset.xvRemoved||(f.remove(),f.dataset.xvRemoved="true",t++)}})});const i=a.select('p[id*="9o6lsm8aj09wav6bf9"]');if(i&&!i.dataset.xvRemoved){const s=i.closest('div[style*="background: rgb(222, 38, 0)"]');s&&(s.remove(),s.dataset.xvRemoved="true",t++)}t>0&&console.log(`[XVideos] 已移除 ${t} 个广告元素`)}static observeAds(){new MutationObserver(m(()=>{this.removeRedBanner(),this.removeMenuAds()},c.ui.debounceDelay)).observe(document.body,{childList:!0,subtree:!0})}}function p(){if(typeof html5player<"u"&&html5player.toggleExpand&&html5player.toggleExpand(),typeof html5player<"u"&&html5player.hlsobj&&html5player.hlsobj.levels&&Object.defineProperties(html5player.hlsobj,{autoLevelEnabled:{value:!1,writable:!1},firstLevel:{value:4,writable:!1}}),typeof html5player<"u"){let n=!1;Object.defineProperty(html5player,"canPlay",{get:()=>n,set:e=>{e&&(html5player.playClicked=!0,html5player.play?.()),n=e}})}}const g=20;function w(){const n=document.querySelector(".listing_filters");if(n)return{node:n,position:"beforebegin"};const e=document.querySelector(".head__menu-line");if(e)return{node:e,position:"afterend"};const t=document.querySelector("#header .header-bottom")||document.querySelector(".header-bottom");return t?{node:t,position:"afterend"}:null}function k(){const n=w();if(n&&!document.getElementById("recent-tags-container")){const e=document.createElement("div");e.id="recent-tags-container",e.style.backgroundColor="#f1f1f1",e.style.padding="5px 10px",e.style.textAlign="left",e.style.borderBottom="1px solid #ddd",e.style.lineHeight="1.8",e.style.whiteSpace="nowrap",e.style.overflowX="auto",e.style.overflowY="hidden",e.style.position="relative",e.style.width="100%",n.node.insertAdjacentElement(n.position,e)}y()}function v(){const n=localStorage.getItem("xvideos_recent_tags");return n?JSON.parse(n):[]}function S(n){localStorage.setItem("xvideos_recent_tags",JSON.stringify(n))}function R(n){let e=v();e=e.filter(t=>t.href!==n.href),e.unshift(n),e.length>g&&(e=e.slice(0,g)),S(e)}function y(){const n=document.getElementById("recent-tags-container");if(!n)return;n.innerHTML="";const e=v(),t=document.createElement("div");if(t.className="xv-tags",n.appendChild(t),e.length>0){const i=document.createElement("span");i.className="xv-title",i.textContent="近期标签:",t.appendChild(i),e.forEach(s=>{const o=document.createElement("a");o.href=s.href,o.textContent=s.text,o.className="xv-tag",t.appendChild(o)})}}document.addEventListener("click",function(n){const e=n.target?.closest("a");if(e&&e.classList.contains("is-keyword")&&e.getAttribute("href")){const t={text:e.textContent?.trim()||"",href:e.getAttribute("href")||""};R(t),y()}});class d{static isEnabled=!1;static init(){const e=this.getCurrentPageType();console.log(`[XVideos] 开始初始化隐藏已观看功能... 当前页面: ${location.pathname}, 页面类型: ${e}`),this.loadState(),this.isListingPage()&&(console.log(`[XVideos] 检测到列表页面,功能状态: ${this.isEnabled?"启用":"禁用"}`),this.setupObserver(),this.isEnabled&&(this.applyHiding(),setTimeout(()=>this.applyHiding(),500),setTimeout(()=>this.applyHiding(),1500),setTimeout(()=>this.applyHiding(),3e3)),console.log("[XVideos] 隐藏已观看功能已初始化(列表页面)")),console.log("[XVideos] 隐藏已观看功能初始化完成")}static loadState(){this.isEnabled=l.get(c.storageKeys.hideWatched,!1)}static toggle(e){this.isEnabled=e,l.set(c.storageKeys.hideWatched,e),e?(console.log("[XVideos] 已启用隐藏已观看功能"),this.applyHiding(),setTimeout(()=>this.applyHiding(),200),setTimeout(()=>this.applyHiding(),800)):(console.log("[XVideos] 已禁用隐藏已观看功能"),this.showAllVideos())}static applyHiding(){if(!this.isEnabled)return;const e=a.selectAll(".thumb-block, .video-container");let t=0;console.log(`[XVideos] 检查 ${e.length} 个视频卡片...`),e.forEach(i=>{if(this.isWatchedCard(i)&&!i.dataset.xvHidden){i.style.display="none",i.dataset.xvHidden="1",t++;const s=this.extractVideoId(i)||i.dataset.id||"unknown";console.log(`[XVideos] 隐藏已观看视频: ${s}`)}}),console.log(`[XVideos] 检查完成: 隐藏 ${t} 个已观看视频`)}static isWatchedCard(e){return e?!!(e.classList.contains("viewed")||e.querySelector(".video-viewed")||e.querySelector(".viewedIcon")):!1}static showAllVideos(){const e=a.selectAll(".thumb-block, .video-container");let t=0;e.forEach(i=>{i.dataset.xvHidden==="1"&&(i.style.display="",delete i.dataset.xvHidden,t++)}),console.log(`[XVideos] 已显示 ${t} 个视频`)}static extractVideoId(e){let t=e.querySelector("img[data-videoid]");if(t&&t.getAttribute("data-videoid"))return t.getAttribute("data-videoid");if(t=e.querySelector('img[id^="pic_"]'),t&&t.id)return t.id.replace("pic_","");if(t=e.querySelector('img[src*="/thumbslll/"], img[src*="/thumbsll/"], img[src*="/thumbsl/"]'),t&&t.src){const o=t.src.match(/\/(\d+)\//);if(o)return o[1]}const i=e.querySelector('a[href*="/video/"]');if(i&&i.href){const o=i.href.match(/\/video\/(\d+)/);if(o)return o[1]}if(e.dataset.id)return e.dataset.id;const s=e.closest('[data-id], [id^="video_"], .thumb-block');if(s){if(s.dataset.id)return s.dataset.id;if(s.id&&s.id.startsWith("video_"))return s.id.replace("video_","")}return null}static setupObserver(){new MutationObserver(m(()=>{this.isEnabled&&this.applyHiding()},c.ui.debounceDelay)).observe(document.body,{childList:!0,subtree:!0})}static isListingPage(){return this.getCurrentPageType()!=="other"}static getCurrentPageType(){const e=location.pathname||"",t=new URLSearchParams(location.search);return e.startsWith("/video")?"video":e.startsWith("/c/")?"category":e.startsWith("/tags")?"tags":e.startsWith("/best")?"best":t.has("k")?"search":e==="/"||e===""?"home":"other"}}class V{static init(){const e=d.getCurrentPageType(),t=this.isListingPage();if(console.log(`[XVideos] MenuIntegration检查 - 页面类型: ${e}, 是否列表页: ${t}`),!t){console.log(`[XVideos] MenuIntegration跳过 - hideWatched: ${c.features.hideWatched}, isListing: ${t}`);return}this.addHideWatchedMenuItem(),setTimeout(()=>{d&&this.updateMenuItemState(l.get(c.storageKeys.hideWatched,!1))},100)}static addHideWatchedMenuItem(){if(a.select("#xv-hide-watched-menu-item"))return;let e=a.select(".head__menu-line__main-menu"),t="main-menu";if(!e&&(e=a.select(".header-bottom-inner"),t="header-bottom-inner",!e&&(e=a.select(".head__menu-line"),t="menu-line",!e&&(e=a.select("header .menu"),t="header-menu",!e&&(e=a.select("header"),t="header",!e))))){console.warn("[XVideos] 未找到任何合适的菜单容器");return}const i=this.createHideWatchedMenuItem(t);e.appendChild(i),console.log(`[XVideos] 已添加隐藏已观看菜单项到: ${t} (${e.className})`)}static createHideWatchedMenuItem(e){const t=l.get(c.storageKeys.hideWatched,!1);if(e==="header-bottom-inner"){const i=a.create("a",{id:"xv-hide-watched-menu-item",className:"header-link",href:"#",onclick:r=>{r.preventDefault(),this.toggleHideWatched()}}),s=a.create("span",{className:`icon-f ${t?"icf-eye icf-red-crossed":"icf-eye"}`}),o=a.create("span",{className:"item-title"},t?"隐藏已观看":"显示已观看");return i.appendChild(s),i.appendChild(o),i}else{const i=a.create("li",{id:"xv-hide-watched-menu-item"}),s=a.create("a",{className:"head__menu-line__main-menu__lvl1",href:"#",onclick:u=>{u.preventDefault(),this.toggleHideWatched()}}),o=a.create("span",{className:`icon-f ${t?"icf-eye icf-red-crossed":"icf-eye"} xv-eye-icon`}),r=a.create("span",{className:"main-cats-title"},t?" 隐藏已观看 ":" 显示已观看 ");return s.appendChild(o),s.appendChild(r),i.appendChild(s),i}}static toggleHideWatched(){const t=!l.get(c.storageKeys.hideWatched,!1);l.set(c.storageKeys.hideWatched,t),this.updateMenuItemState(t),d&&d.toggle(t)}static updateMenuItemState(e){const t=a.select("#xv-hide-watched-menu-item");if(t){const i=t.querySelector(".icon-f"),s=t.querySelector(".main-cats-title")||t.querySelector(".item-title");i&&s&&(e?(i.className=i.className.includes("xv-eye-icon")?"icon-f icf-eye icf-red-crossed xv-eye-icon":"icon-f icf-eye icf-red-crossed",i.style.color="",s.textContent=s.className==="main-cats-title"?" 隐藏已观看 ":"隐藏已观看"):(i.className=i.className.includes("xv-eye-icon")?"icon-f icf-eye xv-eye-icon":"icon-f icf-eye",i.style.color="",s.textContent=s.className==="main-cats-title"?" 显示已观看 ":"显示已观看"))}}static isListingPage(){return d.getCurrentPageType()!=="other"}}class h{static playlistId=null;static csrfRemove=null;static init(){if(!this.isWatchLaterPage()){console.log("[XVideos] 非稍后观看页面,跳过初始化");return}console.log("[XVideos] 开始初始化稍后观看管理器..."),this.hookNetworkForTokens(),this.probePlaylistTokens(),this.setupObserver(),this.addRemoveButtons(),console.log("[XVideos] 稍后观看管理器初始化完成")}static isWatchLaterPage(){return location.pathname.startsWith("/watch-later")}static hookNetworkForTokens(){try{const e=window.fetch;window.fetch=async function(t,i){const s=await e.call(this,t,i);return h.tryCaptureTokensFromResponse(t,s),s}}catch{}try{let e=function(){const i=new t,s=i.open;return i.open=function(o,r){return this.__xv_url=r,s.apply(this,arguments)},i.addEventListener("load",function(){h.tryCaptureTokensFromXHR(this.__xv_url,this)}),i};const t=window.XMLHttpRequest;window.XMLHttpRequest=e}catch{}}static setupObserver(){new MutationObserver(m(()=>{this.addRemoveButtons()},c.ui.debounceDelay)).observe(document.body,{childList:!0,subtree:!0})}static addRemoveButtons(){a.selectAll(".video-container, .thumb-block").forEach(t=>{if(t.dataset.xvRemBtn==="1")return;const i=t.querySelector(".video-thumb, .thumb, a:has(img), .thumb-inside"),s=t.querySelector('img[data-videoid], img[id^="pic_"]');if(!i||!s)return;const o=this.extractVideoId(t);o&&this.addRemoveButton(t,o,i)})}static addRemoveButton(e,t,i){const s=a.create("div",{className:"xv-watchlater-remove",title:"从稍后观看移除",onclick:u=>{u.preventDefault(),u.stopPropagation(),this.removeFromWatchLater(t,e)}}),o=a.create("span",{className:"xv-remove-icon"},"×");s.appendChild(o),getComputedStyle(i).position==="static"&&(i.style.position="relative"),i.appendChild(s),e.dataset.xvRemBtn="1"}static async removeFromWatchLater(e,t){try{if((!this.playlistId||!this.csrfRemove)&&await this.probePlaylistTokens(),!this.playlistId||!this.csrfRemove){console.warn("[XVideos] 无法获取 watch-later 列表ID或csrf");return}const i=`/api/playlists/list/${this.playlistId}/remove/${e}`,s=`csrf=${encodeURIComponent(this.csrfRemove)}`,r=await(await fetch(i,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded; charset=UTF-8",accept:"application/json, text/javascript, */*; q=0.01"},credentials:"same-origin",body:s})).json();r&&r.result?(t&&t.parentElement&&t.parentElement.removeChild(t),console.log(`[XVideos] 已从稍后观看中移除视频: ${e}`)):console.warn("[XVideos] 移除失败",r)}catch(i){console.warn("[XVideos] 移除异常",i)}}static extractVideoId(e){const t=e.querySelector('img[data-videoid], img[id^="pic_"]');if(t){const s=t.getAttribute("data-videoid")||(t.id&&t.id.startsWith("pic_")?t.id.replace("pic_",""):null);if(s)return s}if(e.dataset.id)return e.dataset.id;const i=e.querySelector('a[href*="/video"]');if(i){const s=i.getAttribute("href");if(s){const o=s.match(/\/video(\d+)/);if(o)return o[1]}}return null}static tryCaptureTokensFromResponse(e,t){try{const i=typeof e=="string"?e:e&&e.url?e.url:"";if(!/\/api\/playlists\/list\/(\d+)/.test(i))return;const s=RegExp.$1;t.clone().json().then(o=>{o&&o.list&&o.list.csrf&&o.list.csrf.remove&&(this.playlistId=s,this.csrfRemove=o.list.csrf.remove)}).catch(()=>{})}catch{}}static tryCaptureTokensFromXHR(e,t){try{if(!/\/api\/playlists\/list\/(\d+)/.test(e))return;const i=RegExp.$1,s=t.responseText;if(!s)return;const o=JSON.parse(s);o&&o.list&&o.list.csrf&&o.list.csrf.remove&&(this.playlistId=i,this.csrfRemove=o.list.csrf.remove)}catch{}}static async probePlaylistTokens(){if(!this.playlistId){const e=document.querySelector('a[href*="?pl="]');if(e){const t=new URL(e.href,location.origin).searchParams.get("pl");t&&(this.playlistId=t)}}if(this.playlistId&&!this.csrfRemove)try{const t=await(await fetch(`/api/playlists/list/${this.playlistId}`,{method:"POST",credentials:"same-origin"})).json();t&&t.list&&t.list.csrf&&t.list.csrf.remove&&(this.csrfRemove=t.list.csrf.remove)}catch{}}}console.log("[XVideos] TypeScript版本脚本已加载"),x.init(),_.init(),document.addEventListener("DOMContentLoaded",()=>{console.log("[XVideos] DOM内容已加载,开始初始化功能模块..."),b()&&V.init(),k(),d.init(),h.init(),window.location.href.includes("/video")&&p(),console.log("[XVideos] 功能模块初始化完成")});function b(){const n=location.pathname||"",e=new URLSearchParams(location.search);return!!(n.startsWith("/video")||n.startsWith("/c/")||n.startsWith("/tags")||n.startsWith("/best")||e.has("k")||n==="/"||n==="")}window.addEventListener("load",()=>{console.log("[XVideos] 页面完全加载,进行后续检查..."),b()&&setTimeout(()=>{console.log("[XVideos] 页面加载完成后重新检查眼睛图标"),l.get(c.storageKeys.hideWatched,!1)&&d.toggle(!0)},1e3),window.location.href.includes("/video")&&setTimeout(()=>{console.log("[XVideos] 页面加载完成后重新检查播放器"),typeof html5player<"u"&&html5player&&p()},1e3)}),console.log("[XVideos] TypeScript增强脚本加载完成")})();