IYF Ad Filter

Filter ads on iyf.tv

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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()
})();