XVideos+

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

Verze ze dne 10. 03. 2023. Zobrazit nejnovější verzi.

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