iYF Enhanced

解锁网页端 4K 画质 + 网页全屏 + 画中画 + 去广告

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         iYF Enhanced
// @name:zh-CN   爱壹帆增强
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  解锁网页端 4K 画质 + 网页全屏 + 画中画 + 去广告
// @author       XHXIAIEIN
// @match        *://*.aiyifan.tv/*
// @match        *://*.iyf.tv/*
// @match        *://*.yfsp.tv/*
// @match        *://*.yifan.tv/*
// @match        *://*.wyav.tv/*
// @icon         https://www.google.com/s2/favicons?sz=32&domain=iyf.tv
// @license      MIT
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @run-at       document-start
// ==/UserScript==

(function () {
	'use strict';

	// ═════════════════════════════════════════════
	// §1  4K 画质解锁
	// ═════════════════════════════════════════════
	//
	// 绕过 Angular 质量限制,通过 filter hook 显示 4K 选项,
	// 点击时拦截 Angular 事件,从 ngContext 取 HLS URL 后
	// 直接用 HLS.js 加载 4K 流(3840x1608)

	const pageScript = document.createElement('script');
	pageScript.textContent = `(function(){
        var _origFilter = Array.prototype.filter;

        // ── filter hook: 让 4K 选项显示在质量选择器 UI 中 ──
        Array.prototype.filter = function(fn, thisArg) {
            if (this.length > 0 && this[0] && typeof this[0].bitrate === 'number' && 'path' in this[0]) {
                var has4K = false;
                for (var j = 0; j < this.length; j++) {
                    if (this[j].bitrate > 1080 && this[j].path) { has4K = true; break; }
                }
                if (has4K) {
                    try {
                        if (!fn.call(thisArg, { bitrate: 2160, path: '/t' }, 0, [{ bitrate: 2160, path: '/t' }])) {
                            return _origFilter.call(this, function(item) { return !!item.path; });
                        }
                    } catch(e) {}
                }
            }
            return _origFilter.call(this, fn, thisArg);
        };

        // ── 移除 "(客户端)" 标签 ──
        var cleanObserver = new MutationObserver(function() {
            document.querySelectorAll('.bitrate-text').forEach(function(el) {
                if (el.textContent.indexOf('客户端') > -1) el.remove();
            });
        });
        if (document.body) cleanObserver.observe(document.body, { childList: true, subtree: true });
        else document.addEventListener('DOMContentLoaded', function() {
            cleanObserver.observe(document.body, { childList: true, subtree: true });
        });

        // ── 4K 点击劫持:直接用 HLS.js 加载 ──
        function getVisibleVideo() {
            var videos = document.querySelectorAll('vg-player video');
            for (var i = 0; i < videos.length; i++) {
                if (videos[i].style.display !== 'none' && videos[i].offsetWidth > 100) return videos[i];
            }
            return null;
        }

        function getBitrateData() {
            var qs = document.querySelector('vg-quality-selector');
            if (!qs || !qs.__ngContext__) return null;
            var ctx = qs.__ngContext__;
            for (var i = 0; i < Math.min(ctx.length, 60); i++) {
                var item = ctx[i];
                if (item && typeof item === 'object' && item.bitrates && Array.isArray(item.bitrates)) {
                    return item;
                }
            }
            return null;
        }

        function findOldHls() {
            var vgPlayer = document.querySelector('vg-player');
            if (!vgPlayer || !vgPlayer.__ngContext__) return null;
            var ctx = vgPlayer.__ngContext__;
            for (var i = 0; i < ctx.length; i++) {
                if (ctx[i] && ctx[i].hls && typeof ctx[i].hls.destroy === 'function') {
                    return ctx[i].hls;
                }
            }
            return null;
        }

        function switchToQuality(bitrate) {
            var comp = getBitrateData();
            if (!comp) return false;

            var target = null;
            for (var i = 0; i < comp.bitrates.length; i++) {
                if (comp.bitrates[i].bitrate === bitrate) { target = comp.bitrates[i]; break; }
            }
            if (!target || !target.path || !target.path.result) return false;
            if (typeof Hls === 'undefined') return false;

            var video = getVisibleVideo();
            if (!video) return false;

            var currentTime = video.currentTime;
            var wasPlaying = !video.paused;
            var url = target.path.result;

            var oldHls = window.__iyf_hls || findOldHls();
            if (oldHls) {
                try { oldHls.destroy(); } catch(e) {}
            }
            window.__iyf_hls = null;

            requestAnimationFrame(function() {
                var hls = new Hls();
                hls.loadSource(url);
                hls.attachMedia(video);
                hls.on(Hls.Events.MANIFEST_PARSED, function() {
                    video.currentTime = currentTime;
                    if (wasPlaying) video.play().catch(function(){});
                    var label = document.querySelector('.vg-quality-selector-label');
                    if (label) label.textContent = ' ' + (bitrate > 1440 ? '4K' : bitrate + 'P') + ' ';
                    document.querySelectorAll('vg-quality-selector .item').forEach(function(el) {
                        el.classList.remove('active');
                        var text = el.textContent || '';
                        if ((bitrate > 1440 && text.indexOf('4K') > -1) ||
                            (bitrate <= 1440 && text.indexOf(bitrate + '') > -1)) {
                            el.classList.add('active');
                        }
                    });
                });
                hls.on(Hls.Events.ERROR, function(event, data) {
                    if (data.fatal) hls.destroy();
                });
                window.__iyf_hls = hls;
            });
            return true;
        }

        window.__iyf_switchQuality = switchToQuality;
        window.__iyf_getBitrates = function() {
            var comp = getBitrateData();
            return comp ? comp.bitrates.map(function(b) {
                return { bitrate: b.bitrate, hasPath: !!b.path, isVIP: b.isVIP };
            }) : [];
        };

        // ── 捕获 4K/2K 选项的点击,阻止 Angular 处理 ──
        document.addEventListener('click', function(e) {
            var target = e.target;
            var item = null;

            for (var d = 0; target && d < 5; d++, target = target.parentElement) {
                if (target.classList && target.classList.contains('item') &&
                    target.closest && target.closest('vg-quality-selector')) {
                    item = target;
                    break;
                }
            }
            if (!item) return;

            var text = item.textContent || '';
            var is4K = text.indexOf('4K') > -1;
            var is2K = /2[Kk]|1440/.test(text);

            if (is4K || is2K) {
                e.stopImmediatePropagation();
                e.preventDefault();
                switchToQuality(is4K ? 2160 : 1440);
            }
        }, true);

        // ── 自动选择最高画质 ──
        function autoSelectBest() {
            var comp = getBitrateData();
            if (!comp || !comp.bitrates || comp.bitrates.length === 0) return false;

            var best = null;
            for (var i = 0; i < comp.bitrates.length; i++) {
                var b = comp.bitrates[i];
                if (b.path && (!best || b.bitrate > best.bitrate)) best = b;
            }
            if (!best) return false;

            // 检查当前是否已经是最高画质
            var label = document.querySelector('.vg-quality-selector-label');
            var current = label ? label.textContent.trim() : '';
            var bestLabel = best.bitrate > 1440 ? '4K' : best.bitrate + 'P';
            if (current.indexOf(bestLabel) > -1) return true;

            return switchToQuality(best.bitrate);
        }

        var _autoTries = 0;
        var _autoTimer = setInterval(function() {
            _autoTries++;
            if (autoSelectBest() || _autoTries > 30) clearInterval(_autoTimer);
        }, 2000);

    })();`;
	(document.head || document.documentElement).appendChild(pageScript);
	pageScript.remove();

	// ═════════════════════════════════════════════
	// §2  拦截 "下载客户端" 弹窗(兜底)
	// ═════════════════════════════════════════════

	function blockDownloadDialog() {
		GM_addStyle(`
			.cdk-overlay-pane:has(app-ask-app-download-dialog) { display: none !important; }
			.cdk-global-overlay-wrapper:has(app-ask-app-download-dialog) { display: none !important; }
		`);
		const startObserver = () => {
			new MutationObserver(() => {
				document.querySelectorAll('.cdk-overlay-pane').forEach(pane => {
					if (pane.querySelector('app-ask-app-download-dialog')) {
						const backdrop = pane.parentElement?.previousElementSibling;
						if (backdrop?.classList?.contains('cdk-overlay-backdrop')) backdrop.click();
						pane.style.display = 'none';
					}
				});
			}).observe(document.body, {
				childList: true,
				subtree: true,
			});
		};
		if (document.body) startObserver();
		else document.addEventListener('DOMContentLoaded', startObserver);
	}

	// ═════════════════════════════════════════════
	// §3  样式
	// ═════════════════════════════════════════════

	const ICONS = {
		expand: `<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor"><path d="M3 3h6v2H5v4H3V3zm12 0h6v6h-2V5h-4V3zM3 15h2v4h4v2H3v-6zm16 0h2v6h-6v-2h4v-4z"/></svg>`,
		compress: `<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor"><path d="M9 9H3V7h4V3h2v6zm6-6h2v4h4v2h-6V3zm-6 12v6H7v-4H3v-2h6zm6 0h6v2h-4v4h-2v-6z"/></svg>`,
		pip: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14z"/></svg>`,
	};

	const STYLES = `
		* { scrollbar-width: thin; scrollbar-color: transparent transparent; }
		*:hover { scrollbar-color: rgba(255,255,255,0.2) transparent; }
		::-webkit-scrollbar { width: 6px; height: 6px; }
		::-webkit-scrollbar-track { background: transparent; }
		::-webkit-scrollbar-thumb { background: rgba(255,255,255,0); border-radius: 3px; }
		*:hover::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.18); }
		::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.35); }
		::-webkit-scrollbar-corner { background: transparent; }

		html.iyf-web-fs, html.iyf-web-fs body {
			overflow: hidden !important; margin: 0 !important; padding: 0 !important;
		}
		.web-fullscreen {
			position: fixed !important; inset: 0 !important;
			width: 100% !important; height: 100% !important;
			margin: 0 !important; padding: 0 !important; border: none !important;
			z-index: 999999 !important; background: #000 !important;
			box-sizing: border-box !important;
		}
		.web-fullscreen video { width: 100% !important; height: 100% !important; object-fit: contain !important; }
		.web-fullscreen, .web-fullscreen * { scrollbar-width: none !important; }
		.web-fullscreen::-webkit-scrollbar, .web-fullscreen *::-webkit-scrollbar { display: none !important; }

		/* 小窗高度时隐藏侧边功能面板 */
		@media (max-height: 950px) {
			app-sticky-block { display: none !important; }
		}

		/* 去广告 */
		vg-pause-f .vg-vvk-p { display: none !important; }
		.dabf { display: none !important; }
		.ps.pggf { display: none !important; }

		/* 播放器宽度自适应(仅非悬浮模式) */
		.video-player:not(.float-player) {
			max-width: 100% !important; flex: 1 !important; height: auto !important;
		}
		.video-player:not(.float-player) .aa-videoplayer-wrap {
			height: auto !important;
		}
		.video-player:not(.float-player) .video-container:not(.small) {
			width: 100% !important; height: auto !important;
		}
		.video-player:not(.float-player) .video-box {
			width: 100% !important; aspect-ratio: 16/9 !important;
		}

		/* 清除容器固定高度 */
		.playPageTop, .main, .video-module,
		.v-page-content, .d-flex.w-100.justify-content-center {
			height: auto !important; min-height: 0 !important;
		}

		/* VIP 状态下隐藏冗余元素 */
		.iyf-is-vip app-dn-user-menu-item:has(.iconVIP),
		.iyf-is-vip app-daily-sign-in-button,
		.iyf-is-vip .uploadtable { display: none !important; }

		.web-fs-btn, .pip-btn {
			cursor: pointer; padding: 0 8px; color: #fff;
			display: flex; align-items: center; user-select: none;
		}
		.web-fs-btn:hover, .pip-btn:hover { color: #00a1d6; }

		/* 画中画遮罩 */
		.iyf-pip-overlay {
			position: absolute; top: 0; left: 0; width: 100%; height: 100%;
			background: rgba(0,0,0,0.7); backdrop-filter: blur(12px); z-index: 99999;
			display: flex; align-items: center; justify-content: center;
		}
		.iyf-pip-text {
			color: rgba(255,255,255,0.6); font-size: 18px;
			letter-spacing: 2px;
		}
	`;

	// ═════════════════════════════════════════════
	// §4  画中画
	// ═════════════════════════════════════════════

	function getVisibleVideo() {
		const videos = document.querySelectorAll('vg-player video');
		for (const v of videos) {
			if (v.style.display !== 'none' && v.offsetWidth > 100) return v;
		}
		return null;
	}

	async function togglePiP() {
		if (document.pictureInPictureElement) {
			await document.exitPictureInPicture();
			return;
		}

		const video = getVisibleVideo();
		if (!video) return;

		video.disablePictureInPicture = false;

		if (video.readyState < 1) {
			try {
				await new Promise((resolve, reject) => {
					video.addEventListener('loadedmetadata', resolve, { once: true });
					video.addEventListener('canplay', resolve, { once: true });
					setTimeout(() => reject(new Error('timeout')), 8000);
				});
			} catch (e) {
				return;
			}
		}

		try {
			await video.requestPictureInPicture();

			const player = document.querySelector('vg-player');
			if (player) {
				const overlay = document.createElement('div');
				overlay.className = 'iyf-pip-overlay';
				overlay.innerHTML = '<div class="iyf-pip-text">画中画播放中</div>';
				player.style.position = 'relative';
				player.appendChild(overlay);
			}

			video.addEventListener(
				'leavepictureinpicture',
				() => {
					const ov = document.querySelector('.iyf-pip-overlay');
					if (ov) ov.remove();
				},
				{ once: true }
			);
		} catch (e) {}
	}

	// ═════════════════════════════════════════════
	// §5  播放器控件
	// ═════════════════════════════════════════════

	function initPlayerControls() {
		const player = document.querySelector('vg-player');
		const fullscreenBtn = document.querySelector('vg-fullscreen');
		if (!player || !fullscreenBtn || document.querySelector('.web-fs-btn')) return;

		const fsBtn = document.createElement('div');
		fsBtn.className = 'control-item web-fs-btn';
		fsBtn.title = '网页全屏';
		fsBtn.innerHTML = ICONS.expand;
		fsBtn.addEventListener('click', () => {
			player.classList.toggle('web-fullscreen');
			document.documentElement.classList.toggle('iyf-web-fs', player.classList.contains('web-fullscreen'));
			fsBtn.innerHTML = player.classList.contains('web-fullscreen') ? ICONS.compress : ICONS.expand;
		});
		fullscreenBtn.parentNode.insertBefore(fsBtn, fullscreenBtn);

		if (document.pictureInPictureEnabled) {
			const video = getVisibleVideo();
			if (video) video.disablePictureInPicture = false;
			const pipBtn = document.createElement('div');
			pipBtn.className = 'control-item pip-btn';
			pipBtn.title = '画中画';
			pipBtn.innerHTML = ICONS.pip;
			pipBtn.addEventListener('click', togglePiP);
			fullscreenBtn.parentNode.insertBefore(pipBtn, fsBtn);
		}

		document.addEventListener('keydown', e => {
			if (e.key === 'Escape' && player.classList.contains('web-fullscreen')) {
				player.classList.remove('web-fullscreen');
				document.documentElement.classList.remove('iyf-web-fs');
				fsBtn.innerHTML = ICONS.expand;
			}
		});
	}

	// ═════════════════════════════════════════════
	// §6  响应式视口
	// ═════════════════════════════════════════════

	function fixViewport() {
		const DESIRED = 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=yes';
		let meta = document.querySelector('meta[name="viewport"]');
		if (!meta) {
			meta = document.createElement('meta');
			meta.name = 'viewport';
			(document.head || document.documentElement).appendChild(meta);
		}
		if (meta.content !== DESIRED) meta.content = DESIRED;

		// 清除 Angular 可能设置的 body zoom
		if (document.body && document.body.style.zoom) {
			document.body.style.zoom = '';
		}
	}

	// ═════════════════════════════════════════════
	// §7  启动
	// ═════════════════════════════════════════════

	fixViewport();
	blockDownloadDialog();

	function detectVIP() {
		if (document.querySelector('.premium.vip-icon-box')) {
			document.documentElement.classList.add('iyf-is-vip');
			return true;
		}
		return false;
	}

	function onReady() {
		GM_addStyle(STYLES);
		GM_registerMenuCommand('画中画播放', togglePiP);

		// 清除容器固定高度(Angular 通过 inline style 设置)
		let heightsCleared = false;
		function clearFixedHeights() {
			if (heightsCleared) return;
			for (const sel of ['.playPageTop', '.main', '.video-module', '.v-page-content']) {
				const el = document.querySelector(sel);
				if (el) {
					el.style.minHeight = '0';
					el.style.height = 'auto';
				}
			}
			// 只要 playPageTop 存在就标记完成,不再重复执行
			if (document.querySelector('.playPageTop')) heightsCleared = true;
		}

		// 统一 observer:VIP 检测 + 播放器注入 + 容器高度修正
		let vipDone = detectVIP();
		let playerDone = false;

		const observer = new MutationObserver(() => {
			if (!vipDone) vipDone = detectVIP();
			if (!heightsCleared) clearFixedHeights();
			if (!playerDone && document.querySelector('vg-player')) {
				initPlayerControls();
				playerDone = true;
			}
			// 全部完成后 disconnect
			if (vipDone && heightsCleared && playerDone) observer.disconnect();
		});
		observer.observe(document.body, { childList: true, subtree: true });

		// 兜底:2 秒后尝试一次
		setTimeout(() => {
			clearFixedHeights();
			initPlayerControls();
		}, 2000);

		// 视口守卫:仅监听 <head>,只在 viewport 被改时触发
		new MutationObserver(muts => {
			for (const m of muts) {
				if (m.type === 'attributes' && m.target.matches?.('meta[name="viewport"]')) {
					return fixViewport();
				}
				for (const node of m.addedNodes) {
					if (node.nodeName === 'META' && node.name === 'viewport') {
						return fixViewport();
					}
				}
			}
		}).observe(document.head || document.documentElement, {
			childList: true, attributes: true, attributeFilter: ['content']
		});
	}

	if (document.body) onReady();
	else document.addEventListener('DOMContentLoaded', onReady);
})();