Blocks ViewHub popups and adds some extra spice.
Versione datata
// ==UserScript==
// @name VHAddon
// @version 1.1.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,
},
"video_preview": {
"label": "Show video preview in front page",
"type": "checkbox",
"default": true,
},
"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) {
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 (self.#internal.currentLevel != 0) {
self.#internal.currentLevel = 0;
}
target.muted = "muted";
target.play();
});
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();
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() {
this.stop_preview();
this.start_preview();
},
async start_preview() {
if (!this.is_mounted()) {
return;
}
const user = this.get_user();
if (!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 = new HLSPlayer(stream_key, this.$el.querySelector("video"));
}
},
stop_preview() {
if (this.hls_player) {
this.hls_player.destroy();
this.hls_player = null;
}
}
},
render: function (createElement) {
const self = this;
return createElement(
"div", {
staticClass: "v-responsive",
style: {
width: 320,
},
},
[
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 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")) {
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;
}
}
#get_component_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];
}
}
#on_component_created(component) {
switch (this.#get_component_name(component)) {
case "StreamHub":
if (GM_config.get("block_popups")) {
component.checker_welcome_condition = function () { };
component.checker_task_condition = function () { };
}
break;
case "CatalogHub":
this.#hook_renderer(component, this.#add_config_in_footer);
if (GM_config.get("video_preview")) {
this.#hook_renderer(component, this.#exchange_img_with_hls);
}
this.#remove_blocked_users_from_catalog(component);
break;
}
}
#remove_blocked_users_from_catalog(component) {
component.$watch("streams", function (_, new_value) {
this.$nextTick(() => {
const blocked_users = GM_config.get("blocked_users").split(",");
if (blocked_users.length == 0) {
return;
}
for (const i in this.streams) {
const stream = this.streams[i];
if (blocked_users.includes(stream.username)) {
this.streams.splice(i, 1);
}
}
});
}, { immediate: true, deep: true });
}
#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_preview_image(tag, data) {
return tag == "v-img" && data && data.attrs && typeof data.attrs.src === "string"
&& data.attrs.src.startsWith("/storage/preview");
}
#exchange_img_with_hls(_component, _createElement, tag, data, children) {
if (VHAddon.#is_preview_image(tag, data)) {
return {
tag: "vhaddon_hlspreview",
data: {
props: {
poster: data.attrs.src,
}
},
children: children,
};
}
}
static #is_spacer(tag, data) {
return tag == "div" && data && data.staticClass && typeof data.staticClass === "string"
&& data.staticClass.includes("spacer");
}
#add_config_in_footer(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 (VHAddon.#is_spacer(child.tag, child.data)) {
children.splice(i, 0, config_button);
return;
}
}
children.push(config_button);
}
}
(window.webpackJsonp = window.webpackJsonp || []).push([
["vhaddon"],
{
"vhaddon": (_, __, require) => {
addon = new VHAddon(new Importer(require));
},
},
[
["vhaddon", "chunk-vendors"]
]
]);
})();