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

当前为 2025-03-21 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
}