您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Embeds a VidSrc player into IMDb. Works with all Movies, TV Series, and TV Episode listings. It also has a killswitch that will automatically close the player when your ISP changes to the one you want to monitor for. No media content is hosted by this script; it merely accesses a public search index.
当前为
// ==UserScript== // @name IMDb VidSrc Player & VPN Killswitch // @description Embeds a VidSrc player into IMDb. Works with all Movies, TV Series, and TV Episode listings. It also has a killswitch that will automatically close the player when your ISP changes to the one you want to monitor for. No media content is hosted by this script; it merely accesses a public search index. // @namespace https://www.imdb.com/ // @icon https://vidsrc.net/template/vidsrc-ico.png // @version 1.2.1 // @author develgeek // @license MIT // @match https://*.imdb.com/title/* // @grant none // ==/UserScript== (function () { 'use strict'; class IpInspector { static URL = 'https://get.geojs.io/v1/ip/geo.json'; async getIsp() { const response = await fetch(IpInspector.URL); const parsed = await response.json(); const isp = parsed.organization_name ?? ''; console.debug('ip=%s, isp=%s', parsed.ip, isp); return isp.trim(); } } class IpWatcher { static CHECK_INTERVAL_SECS = 5; static STORAGE_KEY = 'vidsrc_watch_isp'; constructor(inspector) { this.inspector = inspector; } onChange(handler) { const id = window.setInterval(() => { const watchIsp = this.watchIsp; if (!watchIsp) return; this.ispMatches() .then(({isp, matches}) => { if (isp !== watchIsp) { handler(isp, matches); } if (matches) { window.clearInterval(id); } }) }, IpWatcher.CHECK_INTERVAL_SECS * 1000); } async ispMatches() { const isp = await this.inspector.getIsp(); if (!isp) throw new Error('No ISP found'); return { isp, matches: isp.toLowerCase().includes(this.watchIsp.toLowerCase()) }; } get watchIsp() { return window.localStorage.getItem(IpWatcher.STORAGE_KEY); } set watchIsp(isp) { window.localStorage.setItem(IpWatcher.STORAGE_KEY, isp.trim()); } } class ListingParser { constructor(document) { this.document = document; } isSeries() { return this.document.title.indexOf('TV Series') > -1; } isEpisode() { return this.document.title.indexOf('TV Episode') > -1; } get seasonEpisode() { const container = this.document.querySelector('[data-testid="hero-subnav-bar-season-episode-numbers-section"]'); const match = container?.textContent.trim().match(/S(\d+)\.E(\d+)/); return match ? { season: match[1], episode: match[2], } : null; } get seriesId() { const link = this.document.querySelector('[data-testid="hero-title-block__series-link"]'); const match = link?.getAttribute('href').match(/\/title\/(tt\d+)\//); return match?.[1]; } get imdbId() { this.imdbIdCached ??= this.document.location.pathname.split('/')?.[2]; return this.imdbIdCached; } } class UrlBuilder { static BASE_URL = 'https://vidsrc.net/embed'; static AUTO_NEXT_EPISODE_PARAM = 'autonext'; constructor(parser) { this.parser = parser; } get url() { const isSeries = this.parser.isSeries(); const isEpisode = this.parser.isEpisode(); if (!isSeries && !isEpisode) { return this.movieUrl; } return isEpisode ? this.episodeUrl : this.seriesUrl; } get movieUrl() { return `${UrlBuilder.BASE_URL}/movie/${this.parser.imdbId}`; } get episodeUrl() { const seasonEpisode = this.parser.seasonEpisode; const seriesId = this.parser.seriesId; if (!seasonEpisode || !seriesId) return null; const {season, episode} = seasonEpisode; return `${UrlBuilder.BASE_URL}/tv/${seriesId}/${season}/${episode}?${UrlBuilder.AUTO_NEXT_EPISODE_PARAM}`; } get seriesUrl() { return `${UrlBuilder.BASE_URL}/tv/${this.parser.imdbId}?${UrlBuilder.AUTO_NEXT_EPISODE_PARAM}`; } } const ipWatcher = new IpWatcher(new IpInspector()); class PlayerBuilder { styles = () => (` <style> #vidsrc { position: fixed; bottom: 10px; right: 10px; z-index: 9999; background: linear-gradient(120deg, #185494, #EE1D23, #FAAF18, #FFFFFF); border-radius: 6px; filter: drop-shadow(0 10px 8px rgb(0 0 0 / 0.04)) drop-shadow(0 4px 3px rgb(0 0 0 / 0.1)); font-family: Roboto, Helvetica, sans-serif; } #vidsrc .container { padding: 10px; display: flex; flex-direction: column; width: 700px; height: 450px; border: 2px solid rgb(255 255 255 / 20%); border-radius: 6px; } #vidsrc .current-isp { margin-right: 1em; } #vidsrc input.watch-isp { width: 115px; border-color: transparent; border-bottom: 1px solid rgb(255 255 255 / 40%); background: transparent; margin-left: 0.25em; } #vidsrc input.watch-isp:focus { outline: none; } #vidsrc input.watch-isp::placeholder { font-style: italic; color: rgb(0, 0, 0, 0.7); } #vidsrc .close-btn { display: block; background: none; border: none; font-size: 18px; cursor: pointer; } #vidsrc .controls { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5em; } #vidsrc .player { flex-grow: 1; } </style> `); container = () => { const parent = document.createElement('div'); parent.id = 'vidsrc'; const container = document.createElement('div'); container.className = 'container'; parent.appendChild(container); return [parent, container]; }; controls = (parentContainer) => { const ispSpan = document.createElement('span'); ispSpan.className = 'current-isp' const ipInputLabel = document.createElement('label'); ipInputLabel.textContent = 'Monitor For ISP:'; const ipInput = document.createElement('input'); ipInput.className = 'watch-isp'; ipInput.placeholder = 'ISP contains...'; ipInput.value = ipWatcher.watchIsp; ipInput.onchange = (event) => { ipWatcher.watchIsp = event.target.value; }; ipInputLabel.appendChild(ipInput); const closeBtn = document.createElement('button'); closeBtn.className = 'close-btn' closeBtn.innerText = '✖️'; const container = document.createElement('div'); container.className = 'controls'; container.appendChild(ispSpan); container.appendChild(ipInputLabel); container.appendChild(closeBtn); closeBtn.addEventListener('click', () => parentContainer.remove()); return [ispSpan, container]; }; player = (url) => { const player = document.createElement('iframe'); player.className = 'player'; player.src = url; player.referrerPolicy = 'no-referrer'; player.allowFullscreen = true; return player; }; make(url) { const [parent, container] = this.container(); const [ispSpan, controls] = this.controls(container); container.appendChild(controls); const player = this.player(url); return { styles: this.styles(), parent, setCurrentIsp: (isp) => { ispSpan.textContent = `Current ISP: ${isp}`; }, show: () => { container.appendChild(player); }, remove: () => { player.remove(); }, }; } } const urlBuilder = new UrlBuilder( new ListingParser(document) ); const player = new PlayerBuilder().make(urlBuilder.url); document.head.insertAdjacentHTML('beforeend', player.styles); document.body.appendChild(player.parent); // Show the player if the entered ISP doesn't match the current ISP. ipWatcher.ispMatches().then(({isp, matches}) => { player.setCurrentIsp(isp); if (matches || !ipWatcher.watchIsp) return; player.show(); ipWatcher.onChange((isp, matches) => { player.setCurrentIsp(isp); if (matches) { player.remove(); console.info('ISP matches, killed player'); } }); }) })();