IYF Ad Filter

Filter ads on iyf.tv

// ==UserScript==
// @name         IYF Ad Filter
// @namespace    http://tampermonkey.net/
// @version      0.3.12
// @description  Filter ads on iyf.tv
// @description:zh-CN  过滤广告 on iyf.tv
// @author       Dylan Zhang
// @include      https://*.iyf.tv/*
// @include      https://*.yifan.tv/*
// @include      https://*.yfsp.tv/*
// @include      https://*.aiyifan.tv/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=iyf.tv
// @license      MIT
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    /* utilities */
    const utils = {
        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`
            )
        },

        getDeps(el, prop) {
            for(const key in el) {
                if (key.startsWith('__ngContext__')) {
                    const context = el[key]
                    for (const item of context) {
                        if (item && typeof item === 'object' && item[prop]) {
                            return [item[prop], item]
                        }
                    }
                }
            }

            return []
        },

        delegateArray(arr) {
            const delegateProto = Object.create(Array.prototype)
            delegateProto.push = function(){ return 0 }
            Object.setPrototypeOf(arr, delegateProto)
            return arr
        }
    }

    /* router */
    class Router {
        constructor() {
            this.routes = new Map()
            this.initialize()
        }
        initialize() {
            const originalPushState = history.pushState
            const originalReplaceState = history.replaceState
            const pushstateEvent = new Event('pushstate')
            const replacestateEvent = new Event('replacestate')

            // override pushState
            history.pushState = function() {
                const result = originalPushState.apply(this, arguments)
                window.dispatchEvent(pushstateEvent)
                return result
            };

            // override replaceState
            history.replaceState = function() {
                const result = originalReplaceState.apply(this, arguments)
                window.dispatchEvent(replacestateEvent)
                return result
            }

            ;['pushstate', 'replaceState', 'popstate'].forEach(eventName => {
                window.addEventListener(eventName, () => this.handle())
            })
        }
        use(path, handler) {
            const handlers = this.routes.has(path)
                ? [...this.routes.get(path), handler]
                : [handler]

            this.routes.set(path, handlers)
            return this
        }
        once(path, handler) {
            let done = false
            function fn() {
                if (done) return
                done = true
                handler.apply(this, arguments)

            }
            this.use(path, fn)
            return this
        }
        handle(pathname) {
            if (!pathname) pathname = this.getPathname()

            for(const [path, handlers] of this.routes) {
                if (
                    typeof path === 'string' && path === pathname ||
                    path instanceof RegExp && path.test(pathname)
                ) {
                    handlers.forEach(fn => fn())
                    return
                }
            }
        }
        getPathname() {
            const path = location.pathname.split('/')[1]
            return path ? `/${path}` : '/'
        }
    }

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

        bindEvents() {
            window.addEventListener('keydown', this.handleKeyDown.bind(this))
        }
        handleKeyDown(event) {
            if (this.isTyping()) return
            if (!this.vgAPI) return

            switch(event.key) {
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                    this.setPlaybackRate(parseInt(event.key))
                    break
                case 'a': // rewind
                    this.seek(this.getCurrentTime() - this.step)
                    break
                case 'd': // fast forward
                    this.seek(this.getCurrentTime() + this.step)
                    break
                case 'f': // fullscreen
                    this.toggleFullscreen()
                    break
            }
        }

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

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

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

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

    /* page */
    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;
        }
    `
    const indexStyle = `
        .sliders .sec4,
        .sliders .sec4 + .separater,

        app-index app-recommended-news:nth-of-type(2),
        app-index app-classified-top-videos:nth-of-type(1) > app-home-collection,
        app-index div:has(> app-discovery-in-home),
        app-index .new-list {
            display: none!important;
        }
    `
    const playStyle = `
        .video-player > div:last-child,

        .video-player .overlay-logo,
        .video-player vg-pause-f,
        .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;
        }
    `
    const listStyle = `
        #filterDiv,
        .filters {
            width: 100%;
        }

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

    const router = new Router()
    router.once('/', () => {
        GM_addStyle(indexStyle)
    })

    const playRE = /^\/(play|watch)/
    let videoShortcuts = null
    router.once(playRE, () => {
        GM_addStyle(playStyle)
        // shortcuts
        videoShortcuts = new VideoShortcuts()
    })
    .use(playRE, () => {
        const { ensureElement, getDeps, delegateArray } = utils

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

            // remove pause ads for 20s
            const [pgmp] = getDeps(aaVideoPlayerEl, 'pgmp')
            if (pgmp) {
                pgmp.dataList = delegateArray([])
            } else {
                console.warn('pgmp not found')
            }

            // shortcuts
            const [vgAPI] = getDeps(vgPlayerEl, 'API')
            if (vgAPI) {
                videoShortcuts.vgAPI = vgAPI
            } else {
                console.warn('vgAPI not found')
            }
        })
    })

    router.once(/^\/(list|search)/, () => {
        GM_addStyle(listStyle)
    })

    function init() {
        GM_addStyle(commonStyle)
        router.handle()
    }

    init()
})();