Play video on hover

Facebook, Vimeo, Youtube, Streamable, Tiktok, Instagram, Twitter, X, Dailymotion, Coub, Spotify, SoundCloud, Apple Podcasts, Amazon Music, Deezer, Tidal, Ted, Pbs, Odysee, Playeur, Bitchute, Rss - play on hover

// ==UserScript==
// @name         Play video on hover
// @namespace    https://lukaszmical.pl/
// @version      0.6.0
// @description  Facebook, Vimeo, Youtube, Streamable, Tiktok, Instagram, Twitter, X, Dailymotion, Coub, Spotify, SoundCloud, Apple Podcasts, Amazon Music, Deezer, Tidal, Ted, Pbs, Odysee, Playeur, Bitchute, Rss - 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/base/BaseService.ts
const defaultServiceStyle = {
  width: '500px',
  height: '282px',
};
const BaseService = class {
  createUrl(url, params) {
    if (params) {
      return `${url}?${this.params(params)}`;
    }
    return url;
  }

  extractId(url, match) {
    const result = this.match(url, match);
    return result?.id || '';
  }

  isDarkmode() {
    return window.matchMedia('(prefers-color-scheme: dark)').matches;
  }

  match(url, match) {
    const result = url.match(match);
    if (result && result.groups) {
      return result.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/base/ServiceFactory.ts
const ServiceFactory = class extends BaseService {
  constructor(config, styles = defaultServiceStyle) {
    super();
    this.config = config;
    this.styles = styles;
    this.initialStyles = styles;
  }

  bindParams(url, params) {
    return Object.entries(params).reduce(
      (acc, [key, value]) =>
        acc.replace(`:${key}`, value !== void 0 ? `${value}` : ''),
      url
    );
  }

  async embeddedVideoUrl(element) {
    const isDarkMode = this.isDarkmode();
    const patternParams = this.match(element.href, this.config.pattern) || {};
    const urlParams = {
      ...patternParams,
      ...this.urlParams(element),
      theme: isDarkMode ? 'dark' : 'light',
    };
    this.styles = {
      ...this.initialStyles,
      height: this.getHeight(urlParams),
    };
    const embedUrl = this.bindParams(
      this.createUrl(this.config.embedUrl, this.config.queryParams),
      urlParams
    );
    if (this.config.urlFunction) {
      return this.config.urlFunction({
        ...urlParams,
        url: embedUrl,
      });
    }
    return embedUrl;
  }

  isValidUrl(url) {
    return this.config.pattern.test(url);
  }

  getHeight(urlParams) {
    if (this.config.heightFunction) {
      return this.config.heightFunction(urlParams);
    }
    if (this.config.typeHeight && urlParams.type in this.config.typeHeight) {
      return this.config.typeHeight[urlParams.type];
    }
    return this.initialStyles.height;
  }

  urlParams(element) {
    return {
      href: element.href,
      pathname: element.pathname,
      search: element.search,
    };
  }
};

// apps/on-hover-preview/src/services/AmazonMusic.ts
const AmazonMusic = class extends ServiceFactory {
  constructor() {
    super(
      {
        embedUrl: 'https://music.amazon.com/embed/:id',
        pattern:
          /music\.amazon\.com\/(?<type>albums|tracks|artists|playlists)\/(?<id>[^/?]+)/,
        typeHeight: { tracks: '250px' },
      },
      {
        width: '500px',
        borderRadius: '12px',
        height: '372px',
      }
    );
  }
};

// apps/on-hover-preview/src/services/AppleMusic.ts
const AppleMusic = class extends ServiceFactory {
  constructor() {
    super(
      {
        embedUrl: 'https://embed.:service.apple.com:pathname',
        pattern:
          /(?<service>music|podcasts)\.apple\.com\/.{2}\/(?<type>song|music-video|artist|album|podcast)/,
        typeHeight: {
          'music-video': '281px',
          song: '175px',
        },
      },
      {
        width: '500px',
        borderRadius: '12px',
        height: '450px',
      }
    );
  }
};

// apps/on-hover-preview/src/services/Bitchute.ts
const Bitchute = class extends ServiceFactory {
  constructor() {
    super({
      embedUrl: 'https://bitchute.com/embed/:id',
      pattern: /bitchute\.com\/video\/(?<id>[^/?]+)\/?/,
    });
  }
};

// apps/on-hover-preview/src/services/Coub.ts
const Coub = class extends ServiceFactory {
  constructor() {
    super({
      embedUrl: 'https://coub.com/embed/:id',
      pattern: /coub\.com\/view\/(?<id>[^/]+)\/?/,
      queryParams: {
        autostart: 'true',
        muted: 'false',
        originalSize: 'false',
        startWithHD: 'true',
      },
    });
  }
};

// apps/on-hover-preview/src/services/Dailymotion.ts
const Dailymotion = class extends ServiceFactory {
  constructor() {
    super({
      embedUrl: 'https://geo.dailymotion.com/player.html?video=:id',
      pattern: /dailymotion\.com\/video\/(?<id>[^/?]+)/,
    });
  }
};

// apps/on-hover-preview/src/services/Deezer.ts
const Deezer = class extends ServiceFactory {
  constructor() {
    super(
      {
        embedUrl: 'https://widget.deezer.com/widget/:theme/:type/:id',
        pattern:
          /deezer\.com\/.{2}\/(?<type>album|playlist|track|artist|show|episode)\/(?<id>\d+)/,
        queryParams: {
          autoplay: 'true',
          radius: 'true',
          tracklist: 'false',
        },
      },
      {
        width: '500px',
        borderRadius: '10px',
        height: '300px',
      }
    );
  }
};

// apps/on-hover-preview/src/services/Facebook.ts
const Facebook = class extends ServiceFactory {
  constructor() {
    super({
      embedUrl: 'https://www.facebook.com/plugins/video.php',
      pattern: /https:\/\/(www\.|m\.)?facebook\.com\/[\w\-_]+\/videos\//,
      queryParams: {
        width: '500',
        autoplay: 'true',
        href: ':href',
        show_text: 'false',
      },
    });
  }
};

// apps/on-hover-preview/src/services/Instagram.ts
const Instagram = class extends ServiceFactory {
  constructor() {
    super(
      {
        embedUrl: 'https://www.instagram.com/p/:id/embed/',
        pattern: /instagram\.com\/(.+\/)?reel\/(?<id>[^/?]+)/,
      },
      {
        width: '300px',
        height: '500px',
      }
    );
  }
};

// apps/on-hover-preview/src/services/Odysee.ts
const Odysee = class extends ServiceFactory {
  constructor() {
    super({
      embedUrl: 'https://odysee.com/$/embed:pathname',
      pattern: /odysee\.com\/@/,
      queryParams: {
        autoplay: 'true',
      },
    });
  }
};

// apps/on-hover-preview/src/services/Pbs.ts
const Pbs = class extends ServiceFactory {
  constructor() {
    super({
      embedUrl: 'https://player.pbs.org/portalplayer/:id',
      pattern: /pbs\.org\/video\/(?<id>.+)?/,
    });
  }
};

// apps/on-hover-preview/src/services/Playeur.ts
const Playeur = class extends ServiceFactory {
  constructor() {
    super({
      embedUrl: 'https://playeur.com/embed/:id',
      pattern: /playeur\.com\/(v|embed)\/(?<id>[^/]+)\/?/,
    });
  }
};

// apps/on-hover-preview/src/services/Podbean.ts
const Podbean = class extends ServiceFactory {
  constructor() {
    super(
      {
        embedUrl: 'https://www.podbean.com/player-v2',
        pattern: /podbean\.com\/.+\/(?<type>dir|pb)-(?<id>[^/?]+)\/?/,
        queryParams: {
          i: ':id-:type',
        },
      },
      {
        width: '500px',
        height: '150px',
      }
    );
  }
};

// apps/on-hover-preview/src/services/Rss.ts
const Rss = class extends ServiceFactory {
  constructor() {
    super(
      {
        embedUrl: 'https://player.rss.com/:show/:id',
        heightFunction: ({ id }) => (id ? '152px' : '320px'),
        pattern: /rss\.com\/podcasts\/(?<show>[^/]+)\/(?<id>\d*)/,
        queryParams: {
          theme: ':theme',
        },
      },
      {
        width: '500px',
        borderRadius: '8px',
        height: '152px',
      }
    );
  }
};

// apps/on-hover-preview/src/services/SoundCloud.ts
const SoundCloud = class extends ServiceFactory {
  constructor() {
    super(
      {
        embedUrl: 'https://w.soundcloud.com/player',
        pattern: /soundcloud\.com\/[^/]+\/[^/?]+/,
        queryParams: {
          hide_related: 'true',
          auto_play: 'true',
          show_artwork: 'true',
          show_comments: 'false',
          show_teaser: 'false',
          url: ':href',
          visual: 'false',
        },
      },
      {
        width: '600px',
        height: '166px',
      }
    );
  }
};

// apps/on-hover-preview/src/services/Spotify.ts
const Spotify = class extends ServiceFactory {
  constructor() {
    super(
      {
        embedUrl: 'https://open.spotify.com/embed/:type/:id',
        pattern:
          /spotify\.com\/(.+\/)?(?<type>track|album|playlist|episode|artist|show)\/(?<id>[\w-]+)/,
        typeHeight: { track: '152px' },
        urlFunction: ({ type, url }) =>
          ['episode', 'show'].includes(type) ? `${url}/video` : url,
      },
      {
        width: '600px',
        borderRadius: '12px',
        height: '352px',
      }
    );
  }
};

// apps/on-hover-preview/src/services/Streamable.ts
const Streamable = class extends ServiceFactory {
  constructor() {
    super({
      embedUrl: 'https://streamable.com/o/:id',
      pattern: /streamable\.com\/([s|o]\/)?(?<id>[^?/]+).*$/,
      queryParams: {
        autoplay: '1',
      },
    });
  }
};

// apps/on-hover-preview/src/services/Ted.ts
const Ted = class extends ServiceFactory {
  constructor() {
    super({
      embedUrl: 'https://embed.ted.com/talks/:id',
      pattern: /ted\.com\/talks\/(?<id>[^/]+)\/?/,
    });
  }
};

// apps/on-hover-preview/src/services/Tidal.ts
const Tidal = class extends ServiceFactory {
  constructor() {
    super(
      {
        embedUrl: 'https://embed.tidal.com/:types/:id',
        pattern:
          /tidal\.com\/(.+\/)?(?<type>track|album|video|playlist)\/(?<id>\d+|[\w-]+)/,
        typeHeight: {
          video: '281px',
          playlist: '400px',
          track: '120px',
        },
      },
      {
        width: '500px',
        borderRadius: '10px',
        height: '300px',
      }
    );
  }
};

// apps/on-hover-preview/src/services/Tiktok.ts
const Tiktok = class extends ServiceFactory {
  constructor() {
    super(
      {
        embedUrl: 'https://www.tiktok.com/player/v1/:id',
        pattern: /tiktok\.com\/.+\/video\/(?<id>\d+)/,
        queryParams: {
          autoplay: 1,
          rel: 0,
        },
      },
      {
        width: '338px',
        height: '575px',
      }
    );
  }
};

// apps/on-hover-preview/src/services/Twitter.ts
const Twitter = class extends ServiceFactory {
  constructor() {
    super(
      {
        embedUrl: 'https://platform.:platform.com/embed/Tweet.html',
        pattern: /(?<platform>twitter|x)\.com\/.+\/status\/(?<id>\d+)\/video/,
        queryParams: {
          id: ':id',
          maxWidth: 480,
          width: 480,
          theme: ':theme',
        },
      },
      {
        width: '500px',
        height: '300px',
      }
    );
  }
};

// apps/on-hover-preview/src/services/Vimeo.ts
const Vimeo = class extends ServiceFactory {
  constructor() {
    super({
      embedUrl: 'https://player.vimeo.com/video/:id',
      pattern: /vimeo\.com(.+)*\/(?<id>\d+)\/?$/,
    });
  }
};

// apps/on-hover-preview/src/services/Youtube.ts
const YoutubeHelper = class {
  static getId(search) {
    return new URLSearchParams(search).get('v') || '';
  }

  static getStartTime(search) {
    const start = new URLSearchParams(search).get('t') || '0s';
    const result = start.match(/(?:(?<h>\d+)h)?(?:(?<m>\d+)m)?(?<s>\d+)s/);
    if (result && result.groups) {
      return (
        Number(result.groups.h || '0') * 3600 +
        Number(result.groups.m || '0') * 60 +
        Number(result.groups.s || '0')
      );
    }
    return 0;
  }
};
const Youtube = class extends ServiceFactory {
  constructor() {
    super({
      embedUrl: 'https://www.youtube.com/embed/:id',
      pattern: /youtube\.com\/watch/,
      queryParams: {
        autoplay: 1,
        start: ':start',
      },
      urlFunction: ({ search, url }) =>
        this.bindParams(url, {
          id: YoutubeHelper.getId(search),
          start: YoutubeHelper.getStartTime(search),
        }),
    });
  }
};
const YoutubeShortcut = class extends ServiceFactory {
  constructor() {
    super({
      embedUrl: 'https://www.youtube.com/embed/:id',
      pattern: /youtu\.be\/(?<id>[^?/]+)/,
      queryParams: {
        autoplay: 1,
        start: ':start',
      },
      urlFunction: ({ search, url }) =>
        this.bindParams(url, {
          start: YoutubeHelper.getStartTime(search),
        }),
    });
  }
};
const YoutubeShorts = class extends ServiceFactory {
  constructor() {
    super(
      {
        embedUrl: 'https://www.youtube.com/embed/:id',
        pattern: /youtube\.com\/shorts\/(?<id>[^?/]+).*$/,
        queryParams: {
          autoplay: 1,
        },
      },
      {
        width: '256px',
        height: '454px',
      }
    );
  }
};

// apps/on-hover-preview/src/main.ts
function run() {
  const services = [
    Youtube,
    YoutubeShortcut,
    YoutubeShorts,
    Vimeo,
    Streamable,
    Facebook,
    Tiktok,
    Instagram,
    Twitter,
    Dailymotion,
    Dailymotion,
    Coub,
    Spotify,
    SoundCloud,
    AppleMusic,
    Deezer,
    Tidal,
    Ted,
    Pbs,
    Odysee,
    Playeur,
    Bitchute,
    Podbean,
    Rss,
    AmazonMusic,
    // Rumble,
  ].map((Service) => new Service());
  const previewPopup = new PreviewPopup();
  new LinkHover(services, previewPopup.showPopup.bind(previewPopup));
}

if (window.top == window.self) {
  run();
}

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址