Facebook, Vimeo, Youtube, Streamable, Tiktok, Instagram, Twitter, X, Dailymotion, Coub, Spotify, Tableau, SoundCloud, Apple Music, Deezer, Tidal - play on hover
// ==UserScript== // @name Play video on hover // @namespace https://lukaszmical.pl/ // @version 0.5.0 // @description Facebook, Vimeo, Youtube, Streamable, Tiktok, Instagram, Twitter, X, Dailymotion, Coub, Spotify, Tableau, SoundCloud, Apple Music, Deezer, Tidal - play on hover // @author Łukasz Micał // @match *://*/* // @icon https://static-00.iconduck.com/assets.00/cursor-hover-icon-512x439-vou7bdac.png // ==/UserScript== // libs/share/src/ui/SvgComponent.ts const SvgComponent = class { constructor(tag, props = {}) { this.element = Dom.createSvg({ tag, ...props }); } addClassName(...className) { this.element.classList.add(...className); } event(event, callback) { this.element.addEventListener(event, callback); } getElement() { return this.element; } mount(parent) { parent.appendChild(this.element); } }; // libs/share/src/ui/Dom.ts var Dom = class _Dom { static appendChildren(element, children, isSvgMode = false) { if (children) { element.append( ..._Dom.array(children).map((item) => { if (typeof item === 'string') { return document.createTextNode(item); } if (item instanceof HTMLElement || item instanceof SVGElement) { return item; } if (item instanceof Component || item instanceof SvgComponent) { return item.getElement(); } const isSvg = 'svg' === item.tag ? true : 'foreignObject' === item.tag ? false : isSvgMode; if (isSvg) { return _Dom.createSvg(item); } return _Dom.create(item); }) ); } } static applyAttrs(element, attrs) { if (attrs) { Object.entries(attrs).forEach(([key, value]) => { if (value === void 0 || value === false) { element.removeAttribute(key); } else { element.setAttribute(key, `${value}`); } }); } } static applyClass(element, classes) { if (classes) { element.classList.add(...classes.split(' ').filter(Boolean)); } } static applyEvents(element, events) { if (events) { Object.entries(events).forEach(([name, callback]) => { element.addEventListener(name, callback); }); } } static applyStyles(element, styles) { if (styles) { Object.entries(styles).forEach(([key, value]) => { const name = key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`); element.style.setProperty(name, value); }); } } static array(element) { return Array.isArray(element) ? element : [element]; } static create(data) { const element = document.createElement(data.tag); _Dom.appendChildren(element, data.children); _Dom.applyClass(element, data.classes); _Dom.applyAttrs(element, data.attrs); _Dom.applyEvents(element, data.events); _Dom.applyStyles(element, data.styles); return element; } static createSvg(data) { const element = document.createElementNS( 'http://www.w3.org/2000/svg', data.tag ); _Dom.appendChildren(element, data.children, true); _Dom.applyClass(element, data.classes); _Dom.applyAttrs(element, data.attrs); _Dom.applyEvents(element, data.events); _Dom.applyStyles(element, data.styles); return element; } static element(tag, classes, children) { return _Dom.create({ tag, children, classes }); } static elementSvg(tag, classes, children) { return _Dom.createSvg({ tag, children, classes }); } }; // libs/share/src/ui/Component.ts var Component = class { constructor(tag, props = {}) { this.element = Dom.create({ tag, ...props }); } addClassName(...className) { this.element.classList.add(...className); } event(event, callback) { this.element.addEventListener(event, callback); } getElement() { return this.element; } mount(parent) { parent.appendChild(this.element); } }; // apps/on-hover-preview/src/components/PreviewPopup.ts const PreviewPopup = class _PreviewPopup extends Component { constructor() { super('div', { attrs: { id: _PreviewPopup.ID, }, children: { tag: 'iframe', attrs: { allow: 'autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share', allowFullscreen: true, }, styles: { width: '100%', border: 'none', height: '100%', }, }, styles: { width: '500px', background: '#444', boxShadow: 'rgb(218, 218, 218) 1px 1px 5px', display: 'none', height: '300px', overflow: 'hidden', position: 'absolute', zIndex: '9999', }, }); this.iframeActive = false; this.iframe = this.element.children[0]; if (!document.querySelector(`#${_PreviewPopup.ID}`)) { this.mount(document.body); document.addEventListener('click', this.hidePopup.bind(this)); } } static { this.ID = 'play-on-hover-popup'; } hidePopup() { this.iframeActive = false; this.iframe.src = ''; this.element.style.display = 'none'; } showPopup(e, url, service) { if (!this.iframeActive) { this.iframe.src = url; this.iframeActive = true; Dom.applyStyles(this.element, { display: 'block', left: `${e.pageX}px`, top: `${e.pageY}px`, ...service.styles, }); } } }; // libs/share/src/ui/Events.ts const Events = class { static intendHover(validate, mouseover, mouseleave, timeout = 500) { let hover = false; let id = 0; const onHover = (event) => { if (!event.target || !validate(event.target)) { return; } const element = event.target; hover = true; element.addEventListener( 'mouseleave', (ev) => { mouseleave?.call(element, ev); clearTimeout(id); hover = false; }, { once: true } ); clearTimeout(id); id = window.setTimeout(() => { if (hover) { mouseover.call(element, event); } }, timeout); }; document.body.addEventListener('mouseover', onHover); } }; // apps/on-hover-preview/src/helpers/LinkHover.ts const LinkHover = class { constructor(services, onHover) { this.services = services; this.onHover = onHover; Events.intendHover( this.isValidLink.bind(this), this.onAnchorHover.bind(this) ); } anchorElement(node) { if (!(node instanceof HTMLElement)) { return void 0; } if (node instanceof HTMLAnchorElement) { return node; } const parent = node.closest('a'); if (parent instanceof HTMLElement) { return parent; } return void 0; } findService(url = '') { return this.services.find((service) => service.isValidUrl(url)); } isValidLink(node) { const anchor = this.anchorElement(node); if (!anchor || !anchor.href || anchor.href === '#') { return false; } return true; } async onAnchorHover(ev) { const anchor = this.anchorElement(ev.target); if (!anchor) { return; } const service = this.findService(anchor.href); if (!service) { return; } const previewUrl = await service.embeddedVideoUrl(anchor); if (!previewUrl) { return; } this.onHover(ev, previewUrl, service); } }; // apps/on-hover-preview/src/services/BaseService.ts const BaseService = class { extractId(url, match) { const r###lt = url.match(match); if (r###lt) { return r###lt.groups?.id || ''; } return ''; } match(url, match) { const r###lt = url.match(match); if (r###lt && r###lt.groups) { return r###lt.groups; } return void 0; } params(params) { return Object.entries(params) .map(([key, value]) => `${key}=${value}`) .join('&'); } theme(light, dark) { return window.matchMedia('(prefers-color-scheme: dark)').matches ? dark : light; } }; // apps/on-hover-preview/src/services/AppleMusic.ts const AppleMusic = class extends BaseService { constructor() { super(...arguments); this.styles = { width: '500px', borderRadius: '12px', height: '450px', }; this.regExp = /music\.apple\.com\/.{2}\/(?<id>music-video|artist|album)/; } async embeddedVideoUrl({ href, pathname }) { this.setStyle(href); return `https://embed.music.apple.com${pathname}`; } isValidUrl(url) { return this.regExp.test(url); } setStyle(href) { const type = this.extractId(href, this.regExp); if (type === 'music-video') { this.styles.height = '281px'; } else { this.styles.height = '450px'; } } }; // apps/on-hover-preview/src/services/Coub.ts const Coub = class extends BaseService { constructor() { super(...arguments); this.styles = { width: '500px', height: '290px', }; } async embeddedVideoUrl({ href }) { const id = this.extractId(href, /view\/(?<id>[^/]+)\/?/); const params = this.params({ autostart: 'true', muted: 'false', originalSize: 'false', startWithHD: 'true', }); return `https://coub.com/embed/${id}?${params}`; } isValidUrl(url) { return url.includes('coub.com/view'); } }; // apps/on-hover-preview/src/services/Dailymotion.ts const Dailymotion = class extends BaseService { constructor() { super(...arguments); this.styles = { width: '500px', height: '280px', }; } async embeddedVideoUrl(element) { const id = this.extractId(element.href, /video\/(?<id>[^/?]+)[/?]?/); return `https://geo.dailymotion.com/player.html?video=${id}`; } isValidUrl(url) { return url.includes('dailymotion.com/video'); } }; // apps/on-hover-preview/src/services/Deezer.ts const Deezer = class extends BaseService { constructor() { super(...arguments); this.styles = { width: '500px', borderRadius: '10px', height: '300px', }; this.regExp = /deezer\.com\/.{2}\/(?<type>album|playlist|track|artist|podcast|episode)\/(?<id>\d+)/; } async embeddedVideoUrl({ href }) { const theme = this.theme('light', 'dark'); const props = this.match(href, this.regExp); const params = this.params({ autoplay: 'true', radius: 'true', tracklist: 'false', }); if (!props) { return void 0; } return `https://widget.deezer.com/widget/${theme}/${props.type}/${props.id}?${params}`; } isValidUrl(url) { return this.regExp.test(url); } }; // apps/on-hover-preview/src/services/Facebook.ts const Facebook = class extends BaseService { constructor() { super(...arguments); this.styles = { width: '500px', height: '282px', }; } async embeddedVideoUrl(element) { const params = this.params({ width: '500', autoplay: 'true', href: element.href, show_text: 'false', }); return `https://www.facebook.com/plugins/video.php?${params}`; } isValidUrl(url) { return /https:\/\/(www\.|m\.)?facebook\.com\/[\w\-_]+\/videos\//.test(url); } }; // apps/on-hover-preview/src/services/Instagram.ts const Instagram = class extends BaseService { constructor() { super(...arguments); this.styles = { width: '300px', height: '500px', }; } async embeddedVideoUrl({ href }) { const id = this.extractId(href, /reel\/(?<id>[^/]+)\//); return `https://www.instagram.com/p/${id}/embed/`; } isValidUrl(url) { return /instagram\.com\/([a-zA-Z0-9._]{1,30}\/)?reel/.test(url); } }; // apps/on-hover-preview/src/services/SoundCloud.ts const SoundCloud = class extends BaseService { constructor() { super(...arguments); this.styles = { width: '600px', height: '166px', }; } async embeddedVideoUrl({ href }) { const params = this.params({ hide_related: 'true', auto_play: 'true', show_artwork: 'true', show_comments: 'false', show_teaser: 'false', url: encodeURIComponent(href), visual: 'false', }); return `https://w.soundcloud.com/player?${params}`; } isValidUrl(url) { return /soundcloud\.com\/[^/]+\/[^/?]+/.test(url); } }; // apps/on-hover-preview/src/services/Spotify.ts const Spotify = class extends BaseService { constructor() { super(...arguments); this.styles = { width: '600px', borderRadius: '12px', height: '152px', }; this.regExp = /spotify\.com\/(.+\/)?(?<type>track|album|playlist|show)\/(?<id>[\w-]+)/; } async embeddedVideoUrl({ href }) { const props = this.match(href, this.regExp); if (!props) { return void 0; } this.setStyle(props.type); const suffix = props.type === 'show' ? '/video' : ''; return `https://open.spotify.com/embed/${props.type}/${props.id}${suffix}`; } isValidUrl(url) { return this.regExp.test(url); } setStyle(type) { if (type === 'track') { this.styles.height = '152px'; } else if (type === 'album') { this.styles.height = '352px'; } else if (type === 'playlist') { this.styles.height = '352px'; } else if (type === 'show') { this.styles.height = '352px'; } else { this.styles.height = '300px'; } } }; // apps/on-hover-preview/src/services/Streamable.ts const Streamable = class extends BaseService { constructor() { super(...arguments); this.styles = { width: '500px', height: '300px', }; } async embeddedVideoUrl({ href }) { const id = this.extractId(href, /\.com\/([s|o]\/)?(?<id>[^?/]+).*$/); return `https://streamable.com/o/${id}?autoplay=1`; } isValidUrl(url) { return url.includes('streamable.com'); } }; // apps/on-hover-preview/src/services/Tableau.ts const Tableau = class extends BaseService { constructor() { super(...arguments); this.styles = { width: '850px', height: '528px', }; } async embeddedVideoUrl({ href }) { const id = this.extractId(href, /views\/(?<id>[^/]+)\/?/); const params = this.params({ ':animate_transition': 'yes', ':display_count': 'yes', ':display_overlay': 'yes', ':display_spinner': 'yes', ':display_static_image': 'no', ':embed': 'y', ':embed_code_version': '3', ':host_url': 'https%3A%2F%2Fpublic.tableau.com%2F', ':language': 'en-US', ':loadOrderID': '0', ':showVizHome': 'no', ':tabs': 'yes', ':toolbar': 'yes', }); return `https://public.tableau.com/views/${id}/Video?${params}`; } isValidUrl(url) { return url.includes('public.tableau.com/views'); } }; // apps/on-hover-preview/src/services/Tidal.ts const Tidal = class extends BaseService { constructor() { super(...arguments); this.styles = { width: '500px', borderRadius: '10px', height: '300px', }; this.regExp = /tidal\.com\/(.+\/)?(?<type>track|album|video|playlist)\/(?<id>\d+|[\w-]+)/; } async embeddedVideoUrl({ href }) { const props = this.match(href, this.regExp); if (!props) { return void 0; } this.setStyle(props.type); return `https://embed.tidal.com/${props.type}s/${props.id}`; } isValidUrl(url) { return this.regExp.test(url); } setStyle(type) { if (type === 'track') { this.styles.height = '120px'; } else if (type === 'playlist') { this.styles.height = '400px'; } else if (type === 'video') { this.styles.height = '281px'; } else { this.styles.height = '300px'; } } }; // apps/on-hover-preview/src/services/Tiktok.ts const Tiktok = class extends BaseService { constructor() { super(...arguments); this.styles = { width: '338px', height: '575px', }; } async embeddedVideoUrl({ href }) { const id = this.extractId(href, /video\/(?<id>\d+)/); return `https://www.tiktok.com/embed/v2/${id}`; } isValidUrl(url) { return url.includes('tiktok.com') && /video\/\d+/.test(url); } }; // apps/on-hover-preview/src/services/Twitter.ts const Twitter = class extends BaseService { constructor() { super(...arguments); this.styles = { width: '480px', height: '300px', }; } async embeddedVideoUrl({ href }) { const id = this.extractId(href, /status\/(?<id>[^/?]+)[/?]?/); const platform = href.includes('twitter.com') ? 'twitter' : 'x'; const params = this.params({ id, maxWidth: '480', }); return `https://platform.${platform}.com/embed/Tweet.html?${params}`; } isValidUrl(url) { return /https:\/\/(twitter|x)\.com\/.+\/status\/\d+/.test(url); } }; // apps/on-hover-preview/src/services/Vimeo.ts const Vimeo = class extends BaseService { constructor() { super(...arguments); this.styles = { width: '500px', height: '285px', }; } async embeddedVideoUrl(element) { let id = ''; if (/\/\d+(\/.*)?$/.test(element.pathname)) { id = element.pathname.replace(/\D+/g, ''); } else { const response = await fetch( `https://vimeo.com/api/oembed.json?url=${element.href}` ); const data = await response.json(); id = data.video_id; } return `https://player.vimeo.com/video/${id}?autoplay=1`; } isValidUrl(url) { return url.includes('vimeo.com'); } }; // apps/on-hover-preview/src/services/Youtube.ts const Youtube = class _Youtube extends BaseService { constructor() { super(...arguments); this.styles = _Youtube.VideoSize; } static { this.ShortSize = { width: '256px', height: '454px', }; } static { this.VideoSize = { width: '500px', height: '300px', }; } async embeddedVideoUrl({ href, search }) { this.styles = _Youtube.VideoSize; const urlParams = new URLSearchParams(search); let id = urlParams.get('v') || ''; let start = urlParams.get('t') || '0'; if (href.includes('youtu.be')) { id = this.extractId(href, /\.be\/(?<id>[^?/]+).*$/); } else if (href.includes('youtube.com/shorts')) { this.styles = _Youtube.ShortSize; id = this.extractId(href, /youtube\.com\/shorts\/(?<id>[^?/]+).*$/); } else if (href.includes('youtube.com/attribution_link')) { const url = decodeURIComponent(urlParams.get('u') || `/watch?v=${id}`); const attrUrl = new URL(`https://youtube.com${url}`); const attrParams = new URLSearchParams(attrUrl.search); id = attrParams.get('v') || id; start = attrParams.get('t') || start; } if (/(?:(\d+)h)?(?:(\d+)m)?(\d+)s/.test(start)) { const [hour = '0', minutes = '0', seconds = '-1'] = start.match( /(?:(\d+)h)?(?:(\d+)m)?(\d+)s/ ); if (seconds !== '-1') { start = `${(Number(hour) * 60 + Number(minutes)) * 60 + seconds}`; } } const params = this.params({ autoplay: 1, enablejsapi: 1, fs: 1, start, }); return `https://www.youtube.com/embed/${id}?${params}`; } isValidUrl(url) { return ( url.includes('youtube.com/attribution_link') || url.includes('youtube.com/watch') || url.includes('youtube.com/shorts') || url.includes('youtu.be/') ); } }; // apps/on-hover-preview/src/main.ts function run() { const services = [ Youtube, Vimeo, Streamable, Facebook, Tiktok, Instagram, Twitter, Dailymotion, Dailymotion, Coub, Spotify, Tableau, SoundCloud, AppleMusic, Deezer, Tidal, // Odysee, // Rumble, ].map((Service) => new Service()); const previewPopup = new PreviewPopup(); new LinkHover(services, previewPopup.showPopup.bind(previewPopup)); } if (window.top == window.self) { run(); }