您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Blocks ViewHub popups and adds some extra spice.
// ==UserScript== // @name VHAddon // @version 1.2.0 // @description Blocks ViewHub popups and adds some extra spice. // @license GPL // @match https://viewhub.show/* // @grant none // @run-at document-start // @namespace https://greasyfork.org/users/1018597 // @require https://greasyfork.org/scripts/28536-gm-config/code/GM_config.js?version=184529 // ==/UserScript== (() => { "use strict"; GM_config.init({ id: "VHAddonConfig", title: "VHAddon Configuration (refresh after save)", fields: { "block_popups": { "label": "Hide popups on streamer page", "type": "checkbox", "default": true, }, "quality_menu": { "label": "Enable video quality menu", "type": "checkbox", "default": true, }, "lowest_start_quality": { "label": "Start videos with lowest quality if the quality menu feature is active", "type": "checkbox", "default": false, }, "video_preview_type": { "label": "Show video preview in front page", "type": "radio", "options": ["never", "mouseover", "always"], "default": "always", }, "blocked_users": { "label": "Comma separated list of users to hide from the front page", "type": "text", "size": 100, } } }); let addon; class Importer { #load_cache = {}; #require; constructor(require) { this.#require = require; } find_module_id(regex, name) { for (const chunk of window.webpackJsonp) { if (name && !chunk[0].includes(name)) { continue; } const modules = chunk[1]; for (const module_id in modules) { const code = modules[module_id].toString(); if (code.match(regex)) { return module_id; } } } } must_find_module_id(regex, name) { const result = this.find_module_id(regex, name); if (!result) { throw new Error(`Could not find module matching ${regex}` + (name ? ` in chunk "${name}"` : "")); } return result; } module(id) { return this.#require(id).a; } module_es6(id) { return this.#require.n(this.#require(id)); } async chunk(name) { if (this.#load_cache[name] === true) { return; } if (this.#load_cache[name] === undefined) { const self = this; this.#load_cache[name] = (async function () { await self.#require.e(name); self.#load_cache[name] = true; })(); } await this.#load_cache[name]; } } class HLSPlayer { static #hls_module = null; #internal = null; constructor(stream_key, target, level, handler) { if (!HLSPlayer.#hls_module) { throw new Error("Must call HLSPlayer.init and await on it before constructing an HLSPlayer"); } this.#internal = new HLSPlayer.#hls_module.a({ xhrSetup: function (xhr, url) { xhr.open("GET", url + "?token=" + Date.now()) } }); const self = this; this.#internal.on("hlsManifestParsed", () => { if (!self.#internal) { return; // we were destroyed before arriving here } if (handler && handler.levels) { handler.levels.call(self.#internal, self.#internal.levels); } target.muted = "muted"; target.play(); }); if (handler && handler.levelChanged) { this.#internal.on("hlsLevelSwitched", function (_, data) { handler.levelChanged.call(self.#internal, data.level); }); } if (level !== undefined) { this.#internal.startLevel = 0; this.#internal.currentLevel = 0; } this.#internal.attachMedia(target); this.#internal.loadSource(`https://c1565z2457.r-cdn.com/LiveApp/streams/${stream_key}_adaptive.m3u8`); } destroy() { if (this.#internal) { this.#internal.destroy(); this.#internal = null; } } static async init() { if (HLSPlayer.#hls_module) { return; // already initialized } const importer = addon.importer; await importer.chunk("stream"); const hls_module_id = importer.must_find_module_id( /\.\/src\/hls\.ts \*/, "stream", ); HLSPlayer.#hls_module = importer.module_es6(hls_module_id); if (!HLSPlayer.#hls_module) { throw new Error("Failed to load hls module"); } } } const hlspreview = { props: ["poster"], data: function () { return {}; }, beforeUnmount: function () { this.stop_preview(); this.stop_observe(); this.anchor = null; }, mounted: function () { this.anchor = this.get_anchor(); this.start_observer(); if (GM_config.get("video_preview_type") === "always") { this.start_preview(); } }, methods: { is_mounted() { return !!this.anchor; }, get_anchor() { const parent = this.$el.parentElement; if (parent && parent.tagName == "A") { return parent; } throw new Error("parent element is not an anchor"); }, get_user() { const href = this.anchor.getAttribute("href"); const match = href.match(/\/stream\/([0-9a-zA-Z_]+)/); if (match) { return match[1]; } }, start_observer() { if (!this.is_mounted()) { return; } const self = this; this.observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === "attributes" && mutation.attributeName === "href") { self.restart_preview(); } } }); this.observer.observe(this.anchor, { attributes: true }); }, stop_observer() { if (this.observer) { this.observer.disconnect(); this.observer = null; } }, restart_preview() { if (this.hls_player) { this.stop_preview(); this.start_preview(); } }, async start_preview() { if (!this.is_mounted()) { return; } const user = this.get_user(); if (!user || VHAddon.is_blocked_user(user)) { return; } const stream_key = await addon.get_stream_key(user); if (!stream_key || !this.is_mounted()) { return; } await HLSPlayer.init(); if (this.is_mounted() && !this.hls_player) { this.hls_player = new HLSPlayer(stream_key, this.$el.querySelector("video"), 0); } }, stop_preview() { if (this.hls_player) { this.hls_player.destroy(); this.hls_player = null; } }, on_hover(b) { if (GM_config.get("video_preview_type") !== "mouseover") { return; } if (b) { if (this.hls_player) { return; } this.start_preview(); } else { if (!this.hls_player) { return; } this.stop_preview(); } }, }, render: function (createElement) { const self = this; return createElement( "div", { staticClass: "v-responsive", style: { width: 320, }, on: { mouseenter: function () { self.on_hover(true); }, mouseleave: function () { self.on_hover(false); }, } }, [ createElement("div", { staticClass: "v-responsive__sizer", style: { "padding-bottom": "56.25%", } }), createElement("video", { attrs: { poster: self.poster, }, style: { position: "absolute", top: 0, left: 0, "z-index": 0, width: "100%", height: "100%", display: "block", } }), createElement("div", { staticClass: "v-responsive__content", style: { "z-index": 1, }, }, this.$slots.default) ]); } }; class Component { static get_name(component) { if (!component || !component.$vnode || typeof component.$vnode.tag !== "string") { return; } const match = component.$vnode.tag.match(/vue\-component\-\d+\-(.+)/); if (match) { return match[1]; } } static hook_renderer(component, hook_cb) { function patch(renderer) { return function (tag, data, children, normalizationType) { const result = hook_cb(component, renderer, tag, data, children, normalizationType); if (result) { tag = result.tag || tag; data = result.data || data; children = result.children || children; normalizationType = result.normalizationType || normalizationType; } return renderer(tag, data, children, normalizationType); } } if (component._self._c) { component._self._c = patch(component._self._c); } else if (component.$createElement) { component.$createElement = patch(component.$createElement); } else { throw new Error("hook_renderer: no renderer to hook"); } } static is_spacer(tag, data) { return tag == "div" && data && data.staticClass && typeof data.staticClass === "string" && data.staticClass.includes("spacer"); } static add_config_button(component) { Component.hook_renderer(component, function (component, createElement, tag, _data, children) { if (tag != "v-footer") { return; } const config_button = createElement("v-btn", { staticClass: "flexcol mx-auto catalog_footer_btn", attrs: { text: "", height: "54", }, on: { click: function () { GM_config.open(); }, } }, [ createElement("v-icon", { attrs: { size: "32", left: component.$vuetify.breakpoint.smAndUp } }, ["⚙"]), createElement("span", { staticClass: "footer_button_text", }, [component._v(" VHAddon ")]) ]); for (const i in children) { const child = children[i]; if (Component.is_spacer(child.tag, child.data)) { children.splice(i, 0, config_button); return; } } children.push(config_button); }); } } class CatalogHub { #vue; constructor(vue) { this.#vue = vue; this.#remove_blocked_users_from_catalog(); this.#make_video_preview(); Component.add_config_button(vue); } static #is_preview_image(tag, data) { return tag == "v-img" && data && data.attrs && typeof data.attrs.src === "string" && data.attrs.src.startsWith("/storage/preview"); } #make_video_preview() { if (GM_config.get("video_preview_type") !== "never") { Component.hook_renderer(this.#vue, function (_component, _createElement, tag, data, children) { if (CatalogHub.#is_preview_image(tag, data)) { return { tag: "vhaddon_hlspreview", data: { props: { poster: data.attrs.src, }, }, children: children, }; } }); } } #remove_blocked_users_from_catalog() { const vue = this.#vue; vue.$watch("streams", function () { this.$nextTick(function () { const blocked_users = VHAddon.get_blocked_users(); if (blocked_users.length == 0) { return; } for (const i in vue.streams) { const stream = vue.streams[i]; if (blocked_users.includes(stream.username)) { vue.streams.splice(i, 1); } } }); }, { immediate: true, deep: true }); } } class VideoQuality { #id; #data; constructor(id, data) { this.#id = id; this.#data = data; } get id() { return this.#id; } static format(data) { return data.width + "x" + data.height + " @ " + data.bitrate.toLocaleString(); } toString() { if ("string" === typeof this.#data) { return this.#data; } return VideoQuality.format(this.#data); } } class StreamHub { #vue; constructor(vue) { this.#vue = vue; this.#disable_popups(); this.#enable_video_quality_menu(); Component.add_config_button(vue); } #disable_popups() { if (GM_config.get("block_popups")) { this.#vue.checker_welcome_condition = function () { }; this.#vue.checker_task_condition = function () { }; } } #enable_video_quality_menu() { if (!GM_config.get("quality_menu")) { return; } const vue = this.#vue; function create_hls_adapter(stream_id, level) { HLSPlayer.init().then(() => { const video = document.getElementById(vue.hls_video_id); vue.hls_adapter = new HLSPlayer(stream_id, video, level, { "levels": function (levels) { let items = [new VideoQuality(-1, "AUTO")]; for (const level_id in levels) { const level = levels[level_id]; items.push(new VideoQuality(level_id, level)); } vue.stream_resolutions = items; }, "levelChanged": function (id) { const prefix = this.autoLevelEnabled ? "AUTO" : "MANUAL"; vue.now_resolution = prefix + " - " + VideoQuality.format(this.levels[id]); }, }); }); } vue.create_hls_adapter = function (stream_id) { vue.stream_hls_id = stream_id; create_hls_adapter(stream_id, GM_config.get("lowest_start_quality") ? 0 : undefined); }; vue.select_resolution = function (selection) { if (!vue.hls_adapter) { return; } const level = selection.id != -1 ? selection.id : undefined; vue.hls_adapter.destroy(); create_hls_adapter(vue.stream_hls_id, level); }; } } class VHAddon { #importer; #vue; constructor(importer) { this.#importer = importer; this.#vue = importer.module(importer.must_find_module_id( /\/\*\!\s*\*\sVue\.js v\d+\.\d+\.\d+/m, "chunk-vendors", )); const self = this; this.#vue.mixin({ created: function () { self.#on_component_created(this); }, }); if (GM_config.get("video_preview_type") !== "never") { this.#vue.component("vhaddon_hlspreview", hlspreview); } } get importer() { return this.#importer }; get vue() { return this.#vue; } async get_stream_key(user) { const response = await window.fetch(`https://viewhub.show/api/profile/${user}`); const profile = await response.json(); if (profile.status == "ok" && profile.data.online && profile.data.stream_key) { return profile.data.stream_key; } } #on_component_created(component) { switch (Component.get_name(component)) { case "StreamHub": new StreamHub(component); break; case "CatalogHub": new CatalogHub(component); break; } } static get_blocked_users() { return GM_config.get("blocked_users").split(","); } static is_blocked_user(name) { return VHAddon.get_blocked_users().includes(name); } } (window.webpackJsonp = window.webpackJsonp || []).push([ ["vhaddon"], { "vhaddon": (_, __, require) => { addon = new VHAddon(new Importer(require)); }, }, [ ["vhaddon", "chunk-vendors"] ] ]); })();