Greasy Fork

Greasy Fork is available in English.

YFSP.TV Unlocker

Unlocks quality UI, danmu styles (color/type/font/avatar/location), and playback speed UI. Keeps UI responsive when server omits high-bitrate paths. Blocks common ad overlays.

当前为 2026-02-07 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         YFSP.TV Unlocker
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Unlocks quality UI, danmu styles (color/type/font/avatar/location), and playback speed UI. Keeps UI responsive when server omits high-bitrate paths. Blocks common ad overlays.
// @author       YFSP Analyst
// @match        *://*.yfsp.tv/*
// @match        *://*.dudupro.com/*
// @run-at       document-start
// @grant        unsafeWindow
// @homepage     https://github.com/Suysker/scripts-monorepo/tree/main/yfsp
// @supportURL   https://github.com/Suysker/scripts-monorepo/issues
// ==/UserScript==

(function() {
    'use strict';

    const VIP_LEVEL = 99;
    const DEFAULT_USER_ID = 1;
    const DEFAULT_ROLE_ID = 1;
    const MIN_LEVEL = 2;
    const BOOTSTRAP_INTERVAL_MS = 2000;

    const MATCH_USER = [/\/api\/payment\/getPaymentInfo/i, /\/api\/user\/info/i];
    const MATCH_PLAY = [/\/v3\/video\/play/i, /\/v3\/video\/detail/i];
    const STYLE_ID = 'yfsp-unlocker-style';

    const normalizeUrl = (input) => {
        try {
            if (input && typeof input === 'object' && input.url) input = input.url;
        } catch (e) {}

        if (typeof input !== 'string') {
            try {
                input = String(input);
            } catch (e) {
                return '';
            }
        }

        try {
            return new URL(input, location.href).toString();
        } catch (e) {
            return input;
        }
    };

    const shouldMatch = (url, patterns) => patterns.some((pattern) => pattern.test(url));

    const safeToInt = (value) => {
        if (typeof value === 'number') return value;
        const number = parseInt(String(value), 10);
        return Number.isFinite(number) ? number : 0;
    };

    const patchUserState = (user) => {
        if (!user || typeof user !== 'object') return;

        if (user.id == null) user.id = DEFAULT_USER_ID;
        if (user.roleId == null || user.roleId < 0) user.roleId = DEFAULT_ROLE_ID;
        if (user.level == null || user.level < MIN_LEVEL) user.level = MIN_LEVEL;
        if ('isVip' in user) user.isVip = true;
        if ('vipLevel' in user) user.vipLevel = VIP_LEVEL;
    };

    const patchServiceUser = (target) => {
        if (!target || typeof target !== 'object') return;
        if (target._userService && target._userService.user) patchUserState(target._userService.user);
    };

    const patchServiceUserState = (target) => {
        if (!target || typeof target !== 'object') return;
        if (target._userService && target._userService.userState && target._userService.userState._value) {
            patchUserState(target._userService.userState._value);
        }
    };

    const unlockItemFlags = (item) => {
        if (!item || typeof item !== 'object') return;
        item.isVIP = false;
        item.isBought = true;
        item.isEnabled = true;
        if ('isNav' in item) item.isNav = true;
        if ('isLocked' in item) item.isLocked = false;
        if ('lock' in item) item.lock = false;
    };

    const patchUser = (json) => {
        if (!json || !json.data) return json;

        json.data.isVip = true;
        json.data.vipLevel = VIP_LEVEL;
        patchUserState(json.data);

        if (json.data.user && typeof json.data.user === 'object') {
            patchUserState(json.data.user);
        }

        if (Array.isArray(json.data.info)) {
            json.data.info.forEach((info) => {
                if (!info || typeof info !== 'object') return;
                info.isVip = true;
                info.vipLevel = VIP_LEVEL;
                if ('isVip' in info || 'vipLevel' in info || 'id' in info || 'roleId' in info || 'level' in info) {
                    patchUserState(info);
                }
            });
        }

        return json;
    };

    const patchPlay = (json) => {
        if (!json?.data?.info || !Array.isArray(json.data.info)) return json;

        json.data.info.forEach((info) => {
            if (!info || !Array.isArray(info.clarity)) return;

            let best = null;
            info.clarity.forEach((clarity) => {
                if (!clarity || !clarity.path) return;
                if (!best) {
                    best = clarity;
                    return;
                }

                const currentScore = [safeToInt(clarity.qualityIndex), safeToInt(clarity.bitrate), safeToInt(clarity.title)];
                const bestScore = [safeToInt(best.qualityIndex), safeToInt(best.bitrate), safeToInt(best.title)];

                if (
                    currentScore[0] > bestScore[0] ||
                    (currentScore[0] === bestScore[0] &&
                        (currentScore[1] > bestScore[1] ||
                            (currentScore[1] === bestScore[1] && currentScore[2] > bestScore[2])))
                ) {
                    best = clarity;
                }
            });

            info.clarity.forEach((clarity) => {
                if (!clarity) return;

                clarity.isBought = true;
                clarity.isVIP = false;
                clarity.isEnabled = true;
                if (best && !clarity.path && best.path) clarity.path = best.path;
                if (best && best.key && !clarity.key) clarity.key = best.key;
            });
        });

        return json;
    };

    const patchBitrates = (bitrates) => {
        if (!Array.isArray(bitrates)) return false;

        let changed = false;
        bitrates.forEach((bitrate) => {
            if (!bitrate || typeof bitrate !== 'object') return;

            if (bitrate.isVIP === true || bitrate.isBought === false || bitrate.isEnabled === false) {
                bitrate.isVIP = false;
                bitrate.isBought = true;
                bitrate.isEnabled = true;
                changed = true;
            }

            if ('isNav' in bitrate) bitrate.isNav = true;
            if ('isLocked' in bitrate) bitrate.isLocked = false;
            if ('lock' in bitrate) bitrate.lock = false;
        });

        return changed;
    };

    const unlockList = (list) => {
        if (!Array.isArray(list)) return;

        list.forEach((item) => {
            if (!item || typeof item !== 'object') return;
            if ('vipFunction' in item) item.vipFunction = false;
            if ('isDisabled' in item) item.isDisabled = false;
            if ('disabled' in item) item.disabled = false;
            if ('isLocked' in item) item.isLocked = false;
            if ('lock' in item) item.lock = false;
        });
    };

    const safeJson = async (response) => {
        try {
            const clone = response.clone();
            return await clone.json();
        } catch (e) {
            return null;
        }
    };

    const rebuildJsonResponse = (response, payload) =>
        new Response(JSON.stringify(payload), {
            status: response.status,
            statusText: response.statusText,
            headers: response.headers
        });

    const hookFetch = (root) => {
        if (!root || root.__yfsp_fetch_hooked) return;

        const originalFetch = root.fetch;
        if (typeof originalFetch !== 'function') return;

        root.fetch = async function(...args) {
            const requestUrl = normalizeUrl(args[0]);

            if (shouldMatch(requestUrl, MATCH_USER)) {
                const response = await originalFetch.apply(this, args);
                const json = patchUser(await safeJson(response));
                return json ? rebuildJsonResponse(response, json) : response;
            }

            if (shouldMatch(requestUrl, MATCH_PLAY)) {
                const response = await originalFetch.apply(this, args);
                const json = patchPlay(await safeJson(response));
                return json ? rebuildJsonResponse(response, json) : response;
            }

            return originalFetch.apply(this, args);
        };

        root.__yfsp_fetch_hooked = true;
    };

    const hookXhr = (root) => {
        if (!root || root.__yfsp_xhr_hooked) return;

        const proto = root.XMLHttpRequest && root.XMLHttpRequest.prototype;
        if (!proto || proto.__yfsp_patched) return;

        const originalOpen = proto.open;
        const originalSend = proto.send;

        proto.open = function(method, url, ...rest) {
            this.__yfsp_url = normalizeUrl(url);
            return originalOpen.call(this, method, url, ...rest);
        };

        proto.send = function(...sendArgs) {
            const listener = () => {
                if (this.readyState !== 4) return;
                this.removeEventListener('readystatechange', listener);

                const requestUrl = this.__yfsp_url || '';
                if (!requestUrl) return;
                if (!(shouldMatch(requestUrl, MATCH_USER) || shouldMatch(requestUrl, MATCH_PLAY))) return;
                if (this.responseType && this.responseType !== 'text' && this.responseType !== 'json' && this.responseType !== '') return;

                let json = null;
                if (this.responseType === 'json') {
                    if (this.response && typeof this.response === 'object') json = this.response;
                } else {
                    const text = this.responseText;
                    if (!text || text[0] !== '{') return;
                    try {
                        json = JSON.parse(text);
                    } catch (e) {
                        return;
                    }
                }

                json = shouldMatch(requestUrl, MATCH_USER) ? patchUser(json) : patchPlay(json);
                const jsonText = JSON.stringify(json);

                try {
                    Object.defineProperty(this, 'responseText', { configurable: true, get: () => jsonText });
                } catch (e) {}

                try {
                    Object.defineProperty(this, 'response', {
                        configurable: true,
                        get: () => (this.responseType === 'json' ? json : jsonText)
                    });
                } catch (e) {}
            };

            this.addEventListener('readystatechange', listener);
            return originalSend.apply(this, sendArgs);
        };

        proto.__yfsp_patched = true;
        root.__yfsp_xhr_hooked = true;
    };

    const ensureStyle = () => {
        if (document.getElementById(STYLE_ID)) return;

        const style = document.createElement('style');
        style.id = STYLE_ID;
        style.textContent = [
            'iframe[src*="google"] { display: none !important; }',
            'iframe[src*="doubleclick"] { display: none !important; }',
            '.ad, .ads, [id*="ad_"], [class*="ad-"] { display: none !important; }',
            '.use-coin-box { display: none !important; }',
            '#coin-or-upgrade-to-skip-ad { display: none !important; }',
            '.dn-dialog-background { display: none !important; }',
            '#dn_iframe { display: none !important; }',
            'vg-quality-selector .vip-label { display: none !important; }',
            '.quality-btn { opacity: 1 !important; pointer-events: auto !important; }'
        ].join('\n');

        (document.head || document.documentElement).appendChild(style);
    };

    const applyGlobals = (root) => {
        try {
            Object.defineProperty(root, 'isVip', { get: () => true, configurable: true });
            Object.defineProperty(root, 'isAdsBlocked', { get: () => false, configurable: true });
            if (root.User && typeof root.User === 'object') root.User.isVip = true;
        } catch (e) {}
    };

    const hideAds = () => {
        const dialog = document.getElementById('coin-or-upgrade-to-skip-ad');
        if (dialog) dialog.style.display = 'none';

        const dnIframe = document.getElementById('dn_iframe');
        if (dnIframe) dnIframe.style.display = 'none';

        const dialogs = document.querySelectorAll('dn-dialog, .dn-dialog-background');
        dialogs.forEach((el) => {
            el.style.display = 'none';
        });
    };

    const observeDom = () => {
        if (window.__yfsp_observer) return;

        const observer = new MutationObserver(() => {
            ensureStyle();
            hideAds();
        });
        observer.observe(document.documentElement, { childList: true, subtree: true });
        window.__yfsp_observer = observer;
    };

    const findAngularComponent = (selector, matcher) => {
        const element = document.querySelector(selector);
        if (!element || !element.__ngContext__) return null;

        const context = element.__ngContext__;
        if (!Array.isArray(context)) return null;

        return context.find(matcher) || null;
    };

    const patchPlayerComponent = (component) => {
        if (!component || typeof component !== 'object') return;

        if (!component.__yfsp_patched) {
            patchServiceUser(component);
            if (component._user) patchUserState(component._user);

            if (typeof component.changeBitrateIfPossible === 'function') {
                const originalChange = component.changeBitrateIfPossible;
                component.changeBitrateIfPossible = function() {
                    return originalChange.apply(this, arguments);
                };
            }

            component.__yfsp_patched = true;
        }

        const playerProto = Object.getPrototypeOf(component);

        if (playerProto && typeof playerProto.checkIfNeedToggle === 'function' && !playerProto.__yfsp_speed_patched) {
            playerProto.checkIfNeedToggle = function() {
                return true;
            };
            playerProto.__yfsp_speed_patched = true;
        }

        if (playerProto && typeof playerProto.checkIfNeedToggleCallback === 'function' && !playerProto.__yfsp_speed_cb_patched) {
            playerProto.checkIfNeedToggleCallback = function() {
                return true;
            };
            playerProto.__yfsp_speed_cb_patched = true;
        }

        [component.speedList, component.rateList, component.playbackRateList, component.playbackRates, component.speedOptions].forEach(unlockList);

        if (playerProto && !playerProto.__yfsp_speed_methods_patched) {
            Object.getOwnPropertyNames(playerProto).forEach((name) => {
                if (!/speed|rate/i.test(name)) return;

                const fn = playerProto[name];
                if (typeof fn !== 'function') return;
                if (playerProto[`__yfsp_${name}_patched`]) return;

                playerProto[name] = function() {
                    try {
                        if (this._user) patchUserState(this._user);
                        patchServiceUser(this);
                        patchServiceUserState(this);
                    } catch (e) {}
                    return fn.apply(this, arguments);
                };

                playerProto[`__yfsp_${name}_patched`] = true;
            });
            playerProto.__yfsp_speed_methods_patched = true;
        }

        if (typeof component.checkIfNeedToggleCallback === 'function') {
            component.checkIfNeedToggleCallback = function() {
                return true;
            };
        }

        if (
            component.isSwitching === true &&
            component.switching !== true &&
            component.isChanging !== true &&
            component.changeBitrateLoading !== true &&
            component.isLoading !== true &&
            component.loading !== true
        ) {
            component.isSwitching = false;
        }
    };

    const patchQualitySelectorComponent = (component) => {
        if (!component || typeof component !== 'object') return;

        const changed = patchBitrates(component.bitrates);
        if (changed) {
            console.log('[YFSP Unlocker] Angular component patched: bitrates unlocked');
        }

        if (!component._user || typeof component._user !== 'object') {
            component._user = { id: DEFAULT_USER_ID, roleId: DEFAULT_ROLE_ID };
        } else {
            if (component._user.id == null) component._user.id = DEFAULT_USER_ID;
            if (component._user.roleId == null || component._user.roleId < 0) component._user.roleId = DEFAULT_ROLE_ID;
        }

        patchUserState(component._user);
        if ('isVip' in component) component.isVip = true;
        if ('hasVIP' in component) component.hasVIP = true;
        if ('vipLevel' in component) component.vipLevel = VIP_LEVEL;
        patchServiceUser(component);
        patchServiceUserState(component);

        const proto = Object.getPrototypeOf(component);
        if (!proto || typeof proto.selectBitrate !== 'function' || proto.__yfsp_select_patched) return;

        const originalSelect = proto.selectBitrate;
        proto.selectBitrate = function(item) {
            try {
                if (item && typeof item === 'object') unlockItemFlags(item);

                if (this && this._user) patchUserState(this._user);
                if (this) {
                    patchServiceUser(this);
                    patchServiceUserState(this);
                }

                if (item && item.path === null) {
                    console.log('[YFSP Unlocker] 1080P/720P path is null (server-side restriction). Cannot switch.');
                    if (this.bitrates) {
                        const fallback = this.bitrates.find(
                            (bitrate) => bitrate.bitrate === 576 || bitrate.label === '576P' || bitrate.qualityIndex === 0
                        );
                        if (fallback && fallback.path) {
                            console.log('[YFSP Unlocker] Spoofing 1080P with 576P source to bypass null path');
                            item.path = fallback.path;
                        }
                    }
                }
            } catch (e) {}
            return originalSelect.call(this, item);
        };

        proto.__yfsp_select_patched = true;
        console.log('[YFSP Unlocker] Angular component patched: selectBitrate hooked');
    };

    const patchDanmuComponent = (component) => {
        if (!component || typeof component !== 'object') return;

        patchUserState(component.user);
        patchServiceUser(component);

        [component.typeList, component.colorList, component.styleList, component.fontList, component.speedList].forEach(unlockList);
        if ('includeAvatarVip' in component) component.includeAvatarVip = false;
        if ('includeLocationVip' in component) component.includeLocationVip = false;
        if ('includeAvatarLock' in component) component.includeAvatarLock = false;
        if ('includeLocationLock' in component) component.includeLocationLock = false;
        if ('avatarVipFunction' in component) component.avatarVipFunction = false;
        if ('locationVipFunction' in component) component.locationVipFunction = false;

        if (component.danmuFacade && typeof component.danmuFacade === 'object' && !component.danmuFacade.__yfsp_patched) {
            if (typeof component.danmuFacade.updateUserSettings === 'function') {
                const originalUpdate = component.danmuFacade.updateUserSettings;
                component.danmuFacade.updateUserSettings = function() {
                    try {
                        if (component.user) patchUserState(component.user);
                        patchServiceUser(component);
                    } catch (e) {}
                    return originalUpdate.apply(this, arguments);
                };
            }
            component.danmuFacade.__yfsp_patched = true;
        }

        const proto = Object.getPrototypeOf(component);
        if (!proto) return;

        if (typeof proto.selectColor === 'function' && !proto.__yfsp_danmu_color_patched) {
            const originalSelectColor = proto.selectColor;
            proto.selectColor = function(item) {
                try {
                    patchUserState(this.user);
                    patchServiceUser(this);

                    if (item && typeof item === 'object' && this.danmuFacade && typeof this.danmuFacade.setOutputColor === 'function') {
                        this.danmuFacade.setOutputColor(item.value);
                        this.currentColor = item.value;
                        if (typeof this.onFontChanged === 'function') this.onFontChanged();
                        return;
                    }
                } catch (e) {}
                return originalSelectColor.call(this, item);
            };
            proto.__yfsp_danmu_color_patched = true;
        }

        if (typeof proto.selectType === 'function' && !proto.__yfsp_danmu_type_patched) {
            const originalSelectType = proto.selectType;
            proto.selectType = function(item) {
                try {
                    patchUserState(this.user);
                    patchServiceUser(this);
                    if (!this.user && this._userService && this._userService.user) this.user = this._userService.user;

                    if (item && typeof item === 'object' && this.danmuFacade && typeof this.danmuFacade.setOutputType === 'function') {
                        this.danmuFacade.setOutputType(item.value);
                        this.currentType = item.value;
                        if (typeof this.onFontChanged === 'function') this.onFontChanged();
                        return;
                    }
                } catch (e) {}
                return originalSelectType.call(this, item);
            };
            proto.__yfsp_danmu_type_patched = true;
        }

        if (typeof proto.toggleIncludeAvatar === 'function' && !proto.__yfsp_danmu_avatar_patched) {
            const originalToggleAvatar = proto.toggleIncludeAvatar;
            proto.toggleIncludeAvatar = function() {
                try {
                    patchUserState(this.user);
                    patchServiceUser(this);
                    if (!this.user && this._userService && this._userService.user) this.user = this._userService.user;
                    this.includeAvatar = !this.includeAvatar;
                    if (this.danmuFacade && typeof this.danmuFacade.updateUserSettings === 'function') {
                        this.danmuFacade.updateUserSettings({
                            includeAvatar: this.includeAvatar,
                            includeLocation: this.includeLocation
                        });
                        return;
                    }
                } catch (e) {}
                return originalToggleAvatar.call(this);
            };
            proto.__yfsp_danmu_avatar_patched = true;
        }

        if (typeof proto.toggleIncludeLocation === 'function' && !proto.__yfsp_danmu_location_patched) {
            const originalToggleLocation = proto.toggleIncludeLocation;
            proto.toggleIncludeLocation = function() {
                try {
                    patchUserState(this.user);
                    patchServiceUser(this);
                    if (!this.user && this._userService && this._userService.user) this.user = this._userService.user;
                    this.includeLocation = !this.includeLocation;
                    if (this.danmuFacade && typeof this.danmuFacade.updateUserSettings === 'function') {
                        this.danmuFacade.updateUserSettings({
                            includeAvatar: this.includeAvatar,
                            includeLocation: this.includeLocation
                        });
                        return;
                    }
                } catch (e) {}
                return originalToggleLocation.call(this);
            };
            proto.__yfsp_danmu_location_patched = true;
        }
    };

    const patchCommentComponent = (component) => {
        if (!component || typeof component !== 'object') return;

        patchUserState(component.user);
        patchServiceUser(component);
        patchServiceUserState(component);

        const proto = Object.getPrototypeOf(component);
        if (!proto || typeof proto.openVotingCreatorDialog !== 'function' || proto.__yfsp_vote_patched) return;

        const originalOpenVote = proto.openVotingCreatorDialog;
        proto.openVotingCreatorDialog = function() {
            try {
                if (this.user) patchUserState(this.user);
                patchServiceUser(this);
                this.showVotingCreator = true;
                return;
            } catch (e) {}
            return originalOpenVote.call(this);
        };

        proto.__yfsp_vote_patched = true;
    };

    const patchEmojiComponent = (component) => {
        if (!component || typeof component !== 'object') return;

        patchUserState(component.user);
        patchServiceUser(component);

        const proto = Object.getPrototypeOf(component);
        if (!proto || typeof proto.canNotUseVipEmoj !== 'function' || proto.__yfsp_vip_emoji_patched) return;

        proto.canNotUseVipEmoj = function() {
            return false;
        };
        proto.__yfsp_vip_emoji_patched = true;
    };

    const hookAngular = () => {
        try {
            const playerComponent = findAngularComponent(
                'aa-videoplayer',
                (entry) => entry && typeof entry === 'object' && entry.playerMediaListService
            );
            if (playerComponent) patchPlayerComponent(playerComponent);

            const qualityComponent = findAngularComponent(
                'vg-quality-selector',
                (entry) => entry && typeof entry === 'object' && entry.bitrates && entry.bitrateSelected
            );
            if (!qualityComponent) return;
            patchQualitySelectorComponent(qualityComponent);

            const danmuComponent = findAngularComponent(
                'app-danmu-input',
                (entry) => entry && typeof entry === 'object' && entry.typeList && entry.colorList && entry.danmuFacade
            );
            if (danmuComponent) patchDanmuComponent(danmuComponent);

            const commentComponent = findAngularComponent(
                'app-comment-box',
                (entry) => entry && typeof entry === 'object' && entry._commentService && entry._emojiPickerService
            );
            if (commentComponent) patchCommentComponent(commentComponent);

            const emojiComponent = findAngularComponent(
                '.emoji-box',
                (entry) => entry && typeof entry === 'object' && entry._permission && entry.emojiSets
            );
            if (emojiComponent) patchEmojiComponent(emojiComponent);
        } catch (e) {
            console.log('[YFSP Unlocker] Angular hook error:', e);
        }
    };

    const bootstrap = () => {
        hookFetch(window);
        hookXhr(window);

        if (typeof unsafeWindow !== 'undefined') {
            hookFetch(unsafeWindow);
            hookXhr(unsafeWindow);
            applyGlobals(unsafeWindow);
        }

        applyGlobals(window);
        ensureStyle();
        hideAds();
        observeDom();
        hookAngular();
    };

    bootstrap();
    document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
    setInterval(bootstrap, BOOTSTRAP_INTERVAL_MS);
})();