Greasy Fork is available in English.
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.
当前为
// ==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);
})();