XVideos+

ad block, download ability without signing in, remember player settings and fast search

  1. // ==UserScript==
  2. // @name XVideos+
  3. // @namespace -
  4. // @version 2.3.3
  5. // @description ad block, download ability without signing in, remember player settings and fast search
  6. // @author NotYou
  7. // @match *://xvideos.com/*
  8. // @match *://xvideos2.com/*
  9. // @match *://xvideos3.com/*
  10. // @match *://xvideos4.com/*
  11. // @match *://xvideos5.com/*
  12. // @match *://xvideos53.com/*
  13. // @match *://www.xvideos.com/*
  14. // @match *://www.xvideos2.com/*
  15. // @match *://www.xvideos3.com/*
  16. // @match *://www.xvideos4.com/*
  17. // @match *://www.xvideos5.com/*
  18. // @match *://www.xvideos53.com/*
  19. // @run-at document-end
  20. // @license GPL-3.0-or-later
  21. // @grant none
  22. // ==/UserScript==
  23.  
  24. (function() {
  25. const TITLE = 'XVideos+'
  26. const LOCAL_VIDEO_SETTINGS = 'video_settings_xp'
  27.  
  28. const DEFAULT_SETTINGS = JSON.stringify({
  29. volume: 1,
  30. muted: false,
  31. quality: 'Auto',
  32. speed: 1,
  33. loop: false
  34. })
  35.  
  36. class CSS {
  37. static init() {
  38. const _CSS = `
  39. #site-logo-link {
  40. text-decoration: none;
  41. }
  42.  
  43. #plus-xp {
  44. float: right;
  45. position: relative;
  46. margin-top: -10px;
  47. left: -6px;
  48. font-size: 36px;
  49. color: rgb(255, 255, 255);
  50. user-select: none;
  51. height: 0;
  52. }
  53.  
  54. .no-text-dec, .no-text-dec:hover {
  55. text-decoration: none !important;
  56. }
  57.  
  58. #fs-xp {
  59. background-color: rgba(213, 213, 213, .7);
  60. position: fixed;
  61. z-index: 2147483647;
  62. top: 50px;
  63. left: calc(60% / 2); /* 100% - 40% = 60% */
  64. width: 40%;
  65. border-radius: 5px;
  66. }
  67.  
  68. #fs-input-parent-xp {
  69. display: flex;
  70. padding: .5rem;
  71. }
  72.  
  73. #fs-input-parent-xp i {
  74. margin-top: 5px;
  75. margin-right: 5px;
  76. color: rgb(222, 38, 0);
  77. }
  78.  
  79. #fs-input-parent-xp i::before {
  80. font-size: 20px;
  81. }
  82.  
  83. #fs-input-xp {
  84. width: 100%;
  85. border-radius: 4px;
  86. height: 40px;
  87. }
  88.  
  89. .tab-xp {
  90. background-color: rgb(247, 247, 247);
  91. margin: 0 0 10px 0;
  92. padding: 15px 10px;
  93. }
  94.  
  95. #tabDownloadXp {
  96. text-align: center;
  97. font-size: 15px;
  98. }
  99.  
  100. /* DARK-THEME */
  101.  
  102. .dark-theme-xp #fs-xp {
  103. background-color: rgba(0, 0, 0, .7);
  104. }
  105.  
  106. .dark-theme-xp .tab-xp {
  107. background-color: rgb(46, 46, 46);
  108. }`
  109.  
  110. addStyle(_CSS)
  111. }
  112. }
  113.  
  114. class DarkThemeChecker {
  115. static init() {
  116. let isDarkTheme
  117.  
  118. setupDarkThemeClass()
  119.  
  120. const DARK_THEME_OBS = new MutationObserver(setupDarkThemeClass)
  121.  
  122. DARK_THEME_OBS.observe(document.body, {
  123. childList: true,
  124. subtree: true,
  125. })
  126.  
  127. function setupDarkThemeClass() {
  128. setTheme()
  129. setDarkThemeClass()
  130. }
  131.  
  132. function setTheme() {
  133. isDarkTheme = getTheme() === 'black'
  134. }
  135.  
  136. function getTheme() {
  137. const USER_FAVORITE_THEME = localStorage.user_theme_fav
  138. const THEME = JSON.parse(USER_FAVORITE_THEME).i
  139.  
  140. return THEME
  141. }
  142.  
  143. function setDarkThemeClass() {
  144. document.documentElement.classList[isDarkTheme ? 'add' : 'remove']('dark-theme-xp')
  145. }
  146. }
  147. }
  148.  
  149. class AntiAntiAdBlock {
  150. static init() {
  151. Object.defineProperty(window, 'fuckAdBlock', {
  152. get() {
  153. return fakeFuckAdBlockObj()
  154. }
  155. })
  156.  
  157. function fakeFuckAdBlockObj() {
  158. return {
  159. _creatBait: blank('undef'),
  160. _destroyBait: blank('undef'),
  161. _checkBait: blank('undef'),
  162. _log: blank('undef'),
  163. _stopLoop: blank('undef'),
  164.  
  165. onDetected: blank(),
  166. onNotDetected: blank(),
  167. on: blank(),
  168. setOptions: blank(),
  169. setOption: blank(),
  170. check: blank('false'),
  171. emitEvent: blank(),
  172. clearEvent: blank('undef'),
  173. }
  174. }
  175.  
  176. function blank(type) {
  177. let returnValue
  178.  
  179. switch (type) {
  180. case 'undef': break
  181. case 'false': returnValue = false; break
  182. default: returnValue = this; break
  183. }
  184.  
  185. return function() {
  186. return returnValue
  187. }
  188. }
  189. }
  190. }
  191.  
  192. class AdBlock {
  193. static init() {
  194. let css = ''
  195. const SELECTORS = [
  196. '#video-ad',
  197. '#video-ad + div[class]',
  198. '#ad-footer',
  199. '#ad-footer + .remove-ads',
  200. '#ad-footer2',
  201. '#video-sponsor-links',
  202. '.videoad-title',
  203. '.ad-footermobile',
  204. '.ad-support-tablet',
  205. '#ad-header-mobile-contener',
  206. '#v-actions .tabs .dl',
  207. '#content > :first-child > .clearfix',
  208. '.thumb-block-profile > .thumb-inside > .prof-thumb-title',
  209. '.thumb-ad'
  210. ]
  211.  
  212. for (let i = 0; i < SELECTORS.length; i++) {
  213. const SELECTOR = SELECTORS[i]
  214.  
  215. css += SELECTOR + '{ display: none !important; }'
  216. }
  217.  
  218. addStyle(css)
  219. }
  220. }
  221.  
  222. class PlusSymbol {
  223. static init() {
  224. let plus = document.createElement('span')
  225. plus.id = 'plus-xp'
  226. plus.className = 'noselect'
  227. plus.textContent = '+'
  228.  
  229. let svgLogo = document.querySelector('#site-logo')
  230.  
  231. if(svgLogo) {
  232. svgLogo.insertAdjacentElement('afterend', plus)
  233. }
  234. }
  235. }
  236.  
  237. class FastSearch {
  238. static init() {
  239. let fastSearch = document.createElement('div')
  240. let fastSearchInputParent = document.createElement('div')
  241. let fastSearchIcon = document.createElement('i')
  242. let fastSearchInput = document.createElement('input')
  243.  
  244. fastSearch.id = 'fs-xp'
  245. fastSearchInput.id = 'fp-input-xp'
  246. fastSearchInputParent.id = 'fs-input-parent-xp'
  247.  
  248. fastSearchInput.placeholder = 'Fast Search XVideos'
  249. fastSearchInput.className = 'form-control'
  250.  
  251. fastSearchIcon.className = 'icon-f icf-search'
  252.  
  253. fastSearchIcon.addEventListener('click', fastSearchEventHandler)
  254. fastSearchInput.addEventListener('keydown', fastSearchEventHandler)
  255.  
  256. fastSearchInputParent.appendChild(fastSearchIcon)
  257. fastSearchInputParent.appendChild(fastSearchInput)
  258. fastSearch.appendChild(fastSearchInputParent)
  259.  
  260. toggleVisibility(fastSearch)
  261.  
  262. document.body.appendChild(fastSearch)
  263.  
  264. window.addEventListener('keydown', e => {
  265. if(e.code === 'KeyK' && (e.ctrlKey || e.metaKey)) {
  266. e.preventDefault()
  267. toggleVisibility(fastSearch)
  268. fastSearchInput.focus()
  269. }
  270. })
  271.  
  272. function fastSearchEventHandler(e) {
  273. if(e.type === 'click' || e.code === 'Enter') {
  274. search(fastSearchInput.value)
  275. }
  276. }
  277. }
  278. }
  279.  
  280. class CopyableTitle {
  281. static init() {
  282. const VIDEO_TITLE_NODE = document.querySelector('#title-auto-tr') || document.querySelector('.page-title')
  283.  
  284. VIDEO_TITLE_NODE.addEventListener('click', () => {
  285. let videoTitleText = VIDEO_TITLE_NODE.firstChild.textContent.trim()
  286.  
  287. if(!videoTitleText) {
  288. videoTitleText = window.html5player.video_title
  289. }
  290.  
  291. navigator.clipboard.writeText(videoTitleText)
  292. })
  293. }
  294. }
  295.  
  296. class FastButtons {
  297. static init() {
  298. const VIDEO_TITLE = window.html5player.video_title
  299.  
  300. let actionsTabs = document.querySelector('#v-actions .tabs')
  301. let videoTabs = document.querySelector('#video-tabs > .tabs')
  302. let dlBtnOrigin = actionsTabs.querySelector('.dl')
  303.  
  304. let embedBtn = document.createElement('button')
  305. let embedLink = document.createElement('a')
  306.  
  307. let dlBtn = document.createElement('button')
  308. let dlTab = document.createElement('div')
  309. let dlOptions = createDownloadOptions()
  310.  
  311. embedLink.className = 'no-text-dec'
  312. embedLink.target = '_blank'
  313. embedLink.href = '/embedframe/' + window.html5player.id_video
  314. embedLink.innerHTML = '<span class="icon-f icf-embed"></span><span>Embed</span>'
  315.  
  316. dlBtn.className = 'dl-xp'
  317. dlBtn.innerHTML = '<span class="icon-f icf-download"></span><span>Download (XVideos+)</span>'
  318. dlBtn.addEventListener('click', onDlClick)
  319.  
  320. toggleVisibility(dlTab)
  321. dlTab.className = 'tab-xp'
  322. dlTab.id = 'tabDownloadXp'
  323. dlTab.innerHTML = 'Download ' + dlOptions
  324. .createOption('HIGH', window.html5player.url_high, 'High quality')
  325. .createOption('LOW', window.html5player.url_low, 'Low quality')
  326. .create()
  327. + ' quality video.'
  328.  
  329. embedBtn.appendChild(embedLink)
  330. actionsTabs.appendChild(embedBtn)
  331.  
  332. dlBtnOrigin.insertAdjacentElement('beforebegin', dlBtn)
  333. videoTabs.appendChild(dlTab)
  334.  
  335. function onDlClick() {
  336. return toggleVisibility(dlTab)
  337. }
  338.  
  339. function createDownloadOptions() {
  340. let options = ''
  341.  
  342. return {
  343. createOption(label, url, desc) {
  344. options += `<strong><a href="${url}" title="${desc}" target="_blank">${label}</a></strong> / `
  345.  
  346. return this
  347. },
  348.  
  349. create() {
  350. return options.slice(0, -3)
  351. }
  352. }
  353. }
  354. }
  355. }
  356.  
  357. class SaveVideoSettings {
  358. static init() {
  359. let video = window.html5player.video || document.querySelector('#html5video video')
  360.  
  361. class Settings {
  362. static getSettings() {
  363. return JSON.parse(localStorage.getItem(LOCAL_VIDEO_SETTINGS))
  364. }
  365.  
  366. static getItem(key) {
  367. let settings = this.getSettings()
  368.  
  369. return settings[key]
  370. }
  371.  
  372. static setItem(key, value) {
  373. let settings = this.getSettings()
  374.  
  375. settings[key] = value
  376.  
  377. localStorage.setItem(LOCAL_VIDEO_SETTINGS, JSON.stringify(settings))
  378. }
  379. }
  380.  
  381. class Player {
  382. static get volume() {
  383. return video.volume
  384. }
  385.  
  386. static set volume(volume) {
  387. window.html5player.setVolume(volume)
  388. }
  389.  
  390. static get isMuted() {
  391. return video.muted
  392. }
  393.  
  394. static mute() {
  395. return window.html5player.mute()
  396. }
  397.  
  398. static get quality() {
  399. return window.html5player.qualitiesmenubuttonlabel
  400. }
  401.  
  402. static set quality(quality) {
  403. let qualityMenu = window.html5player.qualitymenu
  404.  
  405. if(qualityMenu) {
  406. setQuality()
  407. } else {
  408. let obs = new MutationObserver(() => {
  409. if(window.html5player.qualitymenu) {
  410. qualityMenu = window.html5player.qualitymenu
  411.  
  412. if(qualityMenu) {
  413. setQuality()
  414. obs.disconnect()
  415. }
  416. }
  417. })
  418.  
  419. obs.observe(document.body, {
  420. childList: true,
  421. subtree: true,
  422. })
  423. }
  424.  
  425. function setQuality() {
  426. let qualityEls = qualityMenu.querySelectorAll('span')
  427.  
  428. for (let i = 0; i < qualityEls.length; i++) {
  429. let qualityEl = qualityEls[i]
  430.  
  431. if(qualityEl.textContent.trim().toLowerCase() === quality.toLowerCase()) {
  432. return qualityEl.parentNode.click()
  433. }
  434. }
  435.  
  436. log('Cannot find quality "' + quality + '"')
  437. }
  438. }
  439.  
  440. static get speed() {
  441. return window.html5player.speed
  442. }
  443.  
  444. static set speed(speed) {
  445. window.html5player.speed = speed
  446. }
  447.  
  448. static get isLooped() {
  449. return window.html5player.loopbtn.querySelector('img[src*="1.svg"]') !== null
  450. }
  451.  
  452. static loop() {
  453. let isLooped = this.isLooped
  454.  
  455. if(!isLooped) {
  456. window.html5player.loopbtn.click()
  457. }
  458. }
  459. }
  460.  
  461. restoreVideoSettings()
  462.  
  463. const TARGET_NODE = document.querySelector('#hlsplayer') || document.body
  464.  
  465. let obs = new MutationObserver((mutationList) => {
  466. for (let i = 0; i < mutationList.length; i++) {
  467. const MUTATION_RECORD = mutationList[i]
  468. const TARGET = MUTATION_RECORD.target
  469.  
  470. if(TARGET.matches('.buttons-bar *, .settings-menu *')) {
  471. return setVideoSettings()
  472. }
  473. }
  474. })
  475.  
  476. obs.observe(TARGET_NODE, {
  477. childList: true,
  478. subtree: true,
  479. })
  480.  
  481. function setVideoSettings() {
  482. const VOLUME = Player.volume
  483. const MUTED = Player.isMuted
  484. const QUALITY = Player.quality
  485. const SPEED = Player.speed
  486. const LOOPED = Player.isLooped
  487.  
  488. Settings.setItem('volume', VOLUME)
  489. Settings.setItem('muted', MUTED)
  490. Settings.setItem('quality', QUALITY)
  491. Settings.setItem('speed', SPEED)
  492. Settings.setItem('loop', LOOPED)
  493. }
  494.  
  495. function restoreVideoSettings() {
  496. Player.volume = Settings.getItem('volume')
  497.  
  498. const WAS_MUTED = Settings.getItem('muted')
  499.  
  500. if(WAS_MUTED) {
  501. Player.mute()
  502. }
  503.  
  504. Player.quality = Settings.getItem('quality')
  505. Player.speed = Settings.getItem('speed')
  506.  
  507. const WAS_LOOPED = Settings.getItem('loop')
  508.  
  509. if(WAS_LOOPED) {
  510. Player.loop()
  511. }
  512. }
  513. }
  514. }
  515.  
  516. class BetterVideoPage {
  517. static init() {
  518. const IS_VIDEO = isVideoPage()
  519.  
  520. if(IS_VIDEO) {
  521. const MODULES = [
  522. CopyableTitle,
  523. FastButtons,
  524. SaveVideoSettings
  525. ]
  526.  
  527. initModules(MODULES)
  528. }
  529. }
  530. }
  531.  
  532. class Main {
  533. static init() {
  534. if(!localStorage.getItem(LOCAL_VIDEO_SETTINGS)) {
  535. localStorage.setItem(LOCAL_VIDEO_SETTINGS, DEFAULT_SETTINGS)
  536. }
  537.  
  538. const MODULES = [
  539. CSS,
  540. DarkThemeChecker,
  541. AntiAntiAdBlock,
  542. AdBlock,
  543. PlusSymbol,
  544. FastSearch,
  545. BetterVideoPage
  546. ]
  547.  
  548. initModules(MODULES)
  549.  
  550. log('Loaded')
  551. }
  552. }
  553.  
  554. Main.init()
  555.  
  556. function initModules(modules) {
  557. for (let i = 0; i < modules.length; i++) {
  558. const MODULE = modules[i]
  559.  
  560. initModule(MODULE)
  561. }
  562. }
  563.  
  564. function initModule(module) {
  565. try {
  566. module.init()
  567. } catch(e) {
  568. console.error(TITLE, module.name + ' module, has error:', e)
  569. }
  570. }
  571.  
  572. function addStyle(css) {
  573. let styleNode = document.createElement('style')
  574. styleNode.appendChild(document.createTextNode(css))
  575. document.head.appendChild(styleNode)
  576. }
  577.  
  578. function toggleVisibility(el) {
  579. if(el.style.display === 'none') {
  580. el.style.display = 'block'
  581.  
  582. return void 0
  583. }
  584.  
  585. el.style.display = 'none'
  586. }
  587.  
  588. function log(msg) {
  589. const CSS_LOG_DEFAULT = 'background: rgb(0, 0, 0);font-weight: 800;padding: 1px;'
  590.  
  591. console.log('%cX%cVIDEOS+',
  592. CSS_LOG_DEFAULT + 'color: rgb(222, 38, 0);',
  593. CSS_LOG_DEFAULT + 'color: rgb(255, 255, 255);margin-right: -5px;',
  594. ' -',
  595. msg
  596. )
  597. }
  598.  
  599. function isVideoPage() {
  600. return location.pathname.match(/video\d/)
  601. }
  602.  
  603. function search(q) {
  604. return replaceLocation(location.origin + '/?k=' + toSearchFormat(q))
  605. }
  606.  
  607. function toSearchFormat(str) {
  608. return encodeURIComponent(str.trim()).replace(/%20+/g, '+')
  609. }
  610.  
  611. function replaceLocation(newUrl) {
  612. if(location.replace) {
  613. return location.replace(newUrl)
  614. }
  615.  
  616. location.href = newUrl
  617. }
  618. })();
  619.  
  620.  
  621.  
  622.  
  623.  
  624.  
  625.  
  626.  
  627.  
  628.  
  629.  
  630.