VHAddon

Blocks ViewHub popups and adds some extra spice.

2023-01-29 기준 버전입니다. 최신 버전을 확인하세요.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name            VHAddon
// @version         1.0.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
// ==/UserScript==

(() => {
    "use strict";

    const config = {
        video_preview: true,
        block_popups: true,
    }

    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",
                            width: "100%",
                            heighit: "100%",
                        }
                    }),

                    createElement("div", {
                        staticClass: "v-responsive__content",
                    }, 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 (config.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 (config.block_popups) {
                        component.checker_welcome_condition = function () { };
                        component.checker_task_condition = function () { };
                    }
                    break;
                case "CatalogHub":
                    if (config.video_preview) {
                        this.#hook_renderer(component, this.#exchange_img_with_hls);
                    }
                    break;
            }
        }

        #hook_renderer(component, hook_cb) {
            function patch(renderer) {
                return function (tag, data, children, normalizationType) {
                    const result = hook_cb(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(tag, data, children) {
            if (VHAddon.#is_preview_image(tag, data)) {
                return {
                    tag: "vhaddon_hlspreview",
                    data: {
                        props: {
                            poster: data.attrs.src,
                        }
                    },
                    children: children,
                };
            }
        }
    }

    (window.webpackJsonp = window.webpackJsonp || []).push([
        ["vhaddon"],
        {
            "vhaddon": (_, __, require) => {
                addon = new VHAddon(new Importer(require));
            },
        },
        [
            ["vhaddon", "chunk-vendors"]
        ]
    ]);
})();