爱壹帆广告过滤器

过滤爱壹帆上的广告

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         IYF Ad Filter
// @name:zh-CN   爱壹帆广告过滤器
// @name:zh-TW   愛壹帆廣告過濾器
// @description  Filter ads on iyf.tv
// @description:zh-CN  过滤爱壹帆上的广告
// @description:zh-TW  過濾愛壹帆上的廣告
// @version      0.3.27
// @namespace    https://github.com/dzist
// @author       Dylan Zhang
// @include      *://*.iyf.tv/*
// @include      *://*.yifan.tv/*
// @include      *://*.yfsp.tv/*
// @include      *://*.aiyifan.tv/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=iyf.tv
// @license      MIT
// @run-at       document-end
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    /* utilities */
    const utils = {
        log(message) {
            console.log(
                '%cIYF Ad Filter',
                'padding: 0 4px; background: cyan; color: #000;',
                message
            )
        },
        ensureCondition(condition, maxAttempts = 600 /* 10s */, failureMessage) {
            return new Promise((resolve, reject) => {
                let attempts = 0
                const detect = () => {
                    const result = condition()
                    if (result) {
                        resolve(result)
                    } else if (attempts < maxAttempts) {
                        attempts++
                        requestAnimationFrame(detect)
                    } else {
                        reject(new Error(failureMessage))
                    }
                }
                requestAnimationFrame(detect)
            })
        },
        ensureElement(selector, maxAttempts = 600) {
            return utils.ensureCondition(
                () => document.querySelector(selector),
                maxAttempts,
                `Could not detect ${selector} after ${maxAttempts} attempts`
            )
        },
        fnOnce(fn) {
            let done = false
            return function() {
                if (done) return
                done = true
                fn.apply(this, arguments)
            }
        },
        createEmptyArray() {
            const arr = []
            const proto = Object.create(Array.prototype)
            proto.push = function() { return 0 }
            Object.setPrototypeOf(arr, proto)
            return arr
        },
        getProperty(obj, ...props) {
            const stack = [obj]
            let current = obj

            for(let i = 0, len = props.length; i < len; i++) {
                const key = props[i]
                // check for undefined or null
                if (current == null) {
                    return [, stack]
                }

                const isArraySearch = Array.isArray(current) && isNaN(Number(key))
                if (isArraySearch) {
                    // if the `current` value is an array and the key is not a number,
                    // find the first item with the key inside it.
                    const foundItem = current.find(item => item && item[key])
                    if (foundItem) {
                        stack.push(foundItem)
                        current = foundItem[key]
                    } else {
                        return [, stack]
                    }
                } else {
                    current = current[key]
                    if (i < len - 1 && current) {
                        stack.push(current)
                    }
                }
            }

            return [current, stack]
        }
    }

    /* router */
    class Router {
        constructor() {
            this.routes = new Map()
            this.beforeFns = new Set()
            this.overrideHistory()
        }
        overrideHistory() {
            const { history } = window
            const hook = () => this.handle()
            const override = (fn) => {
                return (...args) => {
                    const result = fn.apply(history, args)
                    hook()
                    return result
                }
            }

            history.pushState = override(history.pushState)
            history.replaceState = override(history.replaceState)
            window.addEventListener('popstate', hook)
        }

        // this.use(path: string | () => void, fn?: () => void): Router
        use(path, fn) {
            let matcher

            if (typeof path === 'string') {
                matcher = this.getMatcher(path)
                if (!matcher) {
                    matcher = {
                        path,
                        regexp: new RegExp(`^${path}$`)
                    }
                }
                this.currentMatcher = matcher
            }
            if (typeof path === 'function') {
                fn = path
                matcher = this.currentMatcher
            }
            if (fn) {
                const fns = this.routes.get(matcher)
                fns
                    ? fns.add(fn)
                    : this.routes.set(matcher, new Set([fn]))
            }

            return this
        }
        getMatcher(path) {
            return this.routes
                .keys()
                .find(m => m.path === path)
        }
        once(path, fn) {
            if (typeof path === 'string' && !fn) {
                throw new Error(
                    `It is not the path, but the corresponding function that can only be used once.` +
                    `Please specify a callback function for the path ${path}`
                )
            }
            if (typeof path === 'function') {
                return this.use(utils.fnOnce(path))
            }

            return this.use(path, utils.fnOnce(fn))
        }
        before(fn) {
            this.beforeFns.add(fn)
            return this
        }
        beforeOnce(fn) {
            return this.before(utils.fnOnce(fn))
        }
        handle(pathname = this.pathname) {
            this.beforeFns.forEach(fn => fn())

            for(const [matcher, fns] of this.routes) {
                if (matcher.regexp.test(pathname)) {
                    fns.forEach(fn => fn())
                }
            }
        }
        get pathname() {
            const path = location.pathname.split('/')[1]
            return path ? `/${path}` : '/'
        }
    }

    /* shortcuts */
    class VideoShortcuts {
        constructor(vgAPI) {
            this.vgAPI = vgAPI
            this.seekStep = 5
            this.volumeStep = 0.1
            this.bindEvents()
        }

        bindEvents() {
            window.addEventListener('keydown', this.handleKeyDown)
            window.addEventListener('beforeunload', this.handleBeforeUnload)
        }
        handleKeyDown = (event) => {
            if (this.isTyping || !this.vgAPI) return

            switch(event.key) {
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                    this.setPlaybackRate(parseInt(event.key))
                    break
                case 'w':
                    this.setVolume(Math.min(this.currentVolume + this.volumeStep, 1))
                    break
                case 's':
                    this.setVolume(Math.max(this.currentVolume - this.volumeStep, 0))
                    break
                case 'a': // rewind
                    this.seek(this.currentTime - this.seekStep)
                    break
                case 'd': // fast forward
                    this.seek(this.currentTime + this.seekStep)
                    break
                case 'f': // fullscreen
                    this.toggleFullscreen()
                    break
            }
        }
        handleBeforeUnload = () => {
            window.removeEventListener('keydown', this.handleKeyDown)
            window.removeEventListener('beforeunload', this.handleBeforeUnload)
            this.vgAPI = null
        }

        get isFullscreen() {
            return this.vgAPI.fsAPI.isFullscreen
        }
        toggleFullscreen() {
            this.isFullscreen
                ? this.exitFullscreen()
                : this.requestFullscreen()
        }
        requestFullscreen() {
            this.vgAPI.fsAPI.request()
        }
        exitFullscreen() {
            this.vgAPI.fsAPI.exit()
        }

        get currentTime() {
            return this.vgAPI.currentTime
        }
        seek(time) {
            this.vgAPI.currentTime = time
        }

        get currentVolume() {
            return this.vgAPI.volume
        }
        setVolume(volume) {
            this.vgAPI.volume = volume
        }

        setPlaybackRate(rate) {
            this.vgAPI.playbackRate = rate
        }

        get isTyping() {
            const activeElement = document.activeElement
            return activeElement instanceof HTMLInputElement
                || activeElement instanceof HTMLTextAreaElement
                || activeElement.isContentEditable
        }
    }

    /* pages */
    const router = new Router()
    router.once('/', () => {
        const indexStyle = `
            .sliders .sec4,
            .sliders .sec4 + .separater,

            app-index app-recommended-news:has(a[href="/svideo"]),
            app-index app-classified-top-videos:has(a[href="/movie"]) > app-home-collection,
            app-index app-classified-top-videos:has(a[href*="/list/short"]),
            app-index div:has(> app-discovery-in-home),
            app-index .new-list {
                display: none!important;
            }
        `
        GM_addStyle(indexStyle)
    })

    let videoShortcuts = null
    router.use('/(play|watch)')
    .once(() => {
        const playStyle = `
            .video-player > div:last-child,

            .video-player vg-pause-f,
            .video-player .overlay-logo,
            .video-player .publicbox,
            .video-player .quanlity-items .use-coin-box {
                display: none!important;
            }

            .video-player .player-title {
                margin-left: 0!important;
            }

            .video-player + div.ps > div.bl {
                display: none!important;
            }

            .main div.playPageTop {
                min-height: 594px!important;
            }
        `
        GM_addStyle(playStyle)
        // shortcuts
        videoShortcuts = new VideoShortcuts()
    })
    .use(() => {
        const { log, ensureElement, getProperty, createEmptyArray } = utils

        Promise.all([
            ensureElement('aa-videoplayer'),
            ensureElement('vg-player'),
            ensureElement('.action-pannel i')
        ]).then(([
            aaVideoPlayerEl,
            vgPlayerEl,
            danmuBtnEl
        ]) => {
            // close danmu
            if (danmuBtnEl.classList.contains('icondanmukai')) {
                danmuBtnEl.click()
            }

            const contextKey = '__ngContext__'

            // remove pause ads for 20s
            const [pgmp] = getProperty(aaVideoPlayerEl, contextKey, 'pgmp')
            if (pgmp) {
                pgmp.dataList = createEmptyArray()
            } else {
                log('pgmp not found')
            }

            // shortcuts
            const [vgAPI] = getProperty(vgPlayerEl, contextKey, 'API')
            if (vgAPI) {
                videoShortcuts.vgAPI = vgAPI
            } else {
                log('vgAPI not found')
            }
        })
    })

    router.once('/(list|search)', () => {
        const listStyle = `
            #filterDiv,
            .filters {
                width: 100%;
            }

            .filters +  div.ss-ctn {
                display: none!important;
            }
        `
        GM_addStyle(listStyle)
    })

    router.beforeOnce(() => {
        const commonStyle = `
            a:has(img[alt="广告"]),
            a[href="https://www.wyav.tv/"],
            i.vip-label {
                display: none!important;
            }

            .navbar .multi-top-buttons,
            .navbar app-dn-user-menu-item.top-item,
            .navbar .login-inner-box,
            .navbar .menu-item:has(a[href="https://www.wyav.tv/"]) {
                display: none!important;
            }
            .navbar .menu-pop.two-col {
                width: 160px!important;
                left: 0!important;
            }
            .navbar .my-card.none-user,
            .navbar .none-user-content {
                height: auto!important;
            }

            .login-frame-container .gg-dl {
                display: none!important;
            }
            .login-frame-container .login-frame-box.heighter,
            .login-frame-container .inner {
                width: auto!important;
                margin-left: 0px!important;
            }

            #sticky-block .inner {
                display: none!important;
            }
        `
        GM_addStyle(commonStyle)
    })

    function main() {
        router.handle()
    }

    main()
})();