import MessageAgent from '../agent' ; import hookEvents from "./events" ; import icons from './icons' ; import AdOptions from './ad-options' ; import { log , err } from './logging' ; import { i18n , seti18n } from './i18n' ; import { AdBlock } from './adblock' ; import { getMetadata , getVideoData } from './fauxapi' ; import Obj from './objutils' ; import { Channel , Settings as _Settings , Action , MutationElement , InfoLink , VideoPoly , Ad , AutoSkipSeconds } from '../typings' ; const enum Layout { Polymer , Unknown } const enum PageType { Video , Channel , Search , Subscriptions , Any } type WhitelistButtonInstance = WhitelistButtonPoly ; type WhitelistButtonFactory = typeof WhitelistButtonPoly ; interface ChannelElement extends HTMLDivElement { whitelistButton : WhitelistButtonPoly ; } let settings ; let pages , watcher , agent , toast ; const ifOrNull = ( tester , val ) => tester ( val ) ? val : null ; const awaitData = ( el ) => new Promise ( resolve => { const _el = el as any ; if ( _el . data ) return resolve ( _el . data ) ; const i = setInterval ( ( ) => { if ( _el . data ) resolve ( _el . data ) ; clearInterval ( i ) ; } , 200 ) ; } ) ; class MutationWatcher { watcher : MutationObserver ; queuedActions : Map < Function , Action > ; firstLoad : boolean ; constructor ( ) { this . watcher = new MutationObserver ( this . onMutation . bind ( this ) ) ; this . queuedActions = new Map ( ) ; this . firstLoad = false ; } start ( ) { this . watcher . observe ( document . body , { childList : true , subtree : true , attributes : true , attributeFilter : [ 'hidden' , 'href' , 'style' , 'class' ] , attributeOldValue : true } ) ; } isPlayerUpdate ( mutation ) { if ( mutation . target . id === 'movie_player' ) { return mutation . target ; } else if ( mutation . target . id === 'player-container' && mutation . addedNodes . length ) { for ( let node of mutation . addedNodes ) { if ( node . id === 'movie_player' ) { return node as HTMLElement ; } } } } isFirstPageLoad ( mutation ) { if ( ! this . firstLoad && mutation . type === 'childList' && mutation . target . localName === 'body' && ( window as any ) . ytplayer ?. config ?. args ?. author ) { this . firstLoad = true ; return true ; } } isPlayerErrorChange ( mutation ) { for ( const node of mutation . addedNodes ) if ( node . nodeType === Node . ELEMENT_NODE && node . classList . contains ( 'ytp-error' ) ) return true ; for ( const node of mutation . removedNodes ) if ( node . nodeType === Node . ELEMENT_NODE && node . classList . contains ( 'ytp-error' ) ) return false ; return null ; } isPolyUserInfo ( mutation ) { const isParent = mutation . target . localName === 'yt-formatted-string' && mutation . target . classList . contains ( 'ytd-channel-name' ) && mutation . addedNodes . length ; const isChild = isParent || ( mutation . target . localName === 'a' && mutation . addedNodes . length && mutation . target . classList . contains ( 'yt-formatted-string' ) && mutation . target . parentElement . classList . contains ( 'ytd-channel-name' ) ) ; if ( isChild ) { const container = mutation . target . closest ( '#top-row ytd-video-owner-renderer' ) ; if ( container ) return container as HTMLElement ; } return null ; } isRelatedUpdate ( mutation ) { return ( mutation . type === 'attributes' && mutation . target . id === 'continuations' && mutation . attributeName === 'hidden' ) ; } hasNewItems ( mutation ) { return ( mutation . type === 'attributes' && mutation . target . localName === 'yt-page-navigation-progress' && mutation . attributeName === 'hidden' && mutation . oldValue === null ) || ( mutation . type === 'childList' && ( mutation . target . id === 'items' || mutation . target . id === 'contents' ) ) ; } isOverlayAd ( mutation ) { return mutation . type === 'childList' && ( mutation . target . classList . contains ( 'ytp-ad-module' ) || mutation . target . classList . contains ( 'ytp-ubo-ad-module' ) ) && mutation . addedNodes . length && mutation . target . querySelector ( 'button.ytp-ad-overlay-close-button' ) ; } fixOverlayVideoAd ( element ) { if ( element . classList . contains ( 'ytp-ad-module' ) || element . classList . contains ( 'ytp-ubo-ad-module' ) ) { if ( element . classList . contains ( 'video-ads' ) ) { element . classList . remove ( 'video-ads' ) ; } element . classList . replace ( 'ytp-ad-module' , 'ytp-ubo-ad-module' ) ; return true ; } return element . classList . contains ( 'video-ads' ) ; } isAdSkipContainer ( mutation ) { return ( mutation . target . classList . contains ( 'ytp-ad-skip-button-container' ) && mutation . target ) || ( mutation . type === 'childList' && this . fixOverlayVideoAd ( mutation . target ) && mutation . addedNodes . length && mutation . target . querySelector ( '.ytp-ad-skip-button-container' ) ) ; } isSubscribeBtn ( mutation ) { return mutation . type === 'childList' && mutation . target . localName === 'yt-formatted-string' && mutation . target . parentElement . localName === 'paper-button' && mutation . target . parentElement . parentElement . localName === 'ytd-subscribe-button-renderer' ? ! ! mutation . target . parentElement . getAttribute ( 'subscribed' ) : undefined ; } isChannelGrid ( mutation ) { return mutation . type === 'childList' && mutation . target . id === 'contents' && Array . from ( mutation . addedNodes ) . some ( el => el . localName === 'ytd-item-section-renderer' ) ; } isNextVideo ( mutation ) { return mutation . type === 'childList' && mutation . target . id === 'movie_player' && mutation . addedNodes . length && Array . from ( mutation . addedNodes ) . some ( el => el . classList . contains ( 'ytp-ce-video' ) ) && Array . from ( mutation . target . querySelectorAll ( '.ytp-ce-covering-overlay' ) ) ; } static adSkipButton ( container ) { return container . style . display !== 'none' && container . querySelector ( 'button' ) ; } pollUpdate ( method ) { let ticket ; let action = ( ) => { ticket . lastExecuted = Date . now ( ) ; method ( ) ; } ; if ( this . queuedActions . has ( method ) ) { ticket = this . queuedActions . get ( method ) ; if ( ticket . timeoutId ) { clearTimeout ( ticket . timeoutId ) ; } if ( Date . now ( ) - ticket . lastExecuted > 400 ) { action ( ) ; } else { ticket . timeoutId = window . setTimeout ( action , 50 ) ; } } else { ticket = { method : method , lastExecuted : Date . now ( ) , timeoutId : window . setTimeout ( action , 50 ) } ; this . queuedActions . set ( method , ticket ) ; } } findInjection ( mutation , selector , cb = ( ) => { } ) { const target = mutation . target as HTMLElement ; if ( target . matches ( selector ) ) { if ( mutation . type === 'attributes' ) { console . log ( `%c[${ selector }].${ mutation . attributeName }` + ` = %c"${ mutation . oldValue }"` + ` -> %c"${ target . getAttribute ( mutation . attributeName || "" ) }"` , 'font-weight: bold' , 'font-weight: normal; color: red;' , 'color: green;' ) ; } else { console . log ( `%c[${ selector }] >` , 'font-weight: bold;' , mutation . type , mutation ) ; } cb ( target ) ; } else if ( mutation . type === 'childList' ) { for ( let node of mutation . addedNodes as NodeListOf < HTMLElement > ) { if ( node . nodeType !== Node . ELEMENT_NODE ) continue ; if ( node . matches ( selector ) ) { console . log ( `<${ this . getSelector ( target ) }>.addedNodes = [...,` + `%c<${ selector }>%c, ...]` , 'color: green;' , mutation ) ; cb ( node ) ; } else if ( node . querySelector ( selector ) ) { console . log ( `<${ this . getSelector ( target ) }>.addedNodes = [...,` + `%c<${ this . getSelector ( node ) }>%c.querySelector("%c${ selector }"%c),...]` , 'color: green;' , '' , 'color: green; font-weight: bold;' , mutation ) ; cb ( node . querySelector ( selector ) ) ; } } for ( let node of mutation . removedNodes as NodeListOf < HTMLElement > ) { if ( node . nodeType !== Node . ELEMENT_NODE ) continue ; if ( node . matches ( selector ) ) { console . log ( `<${ this . getSelector ( target ) }>.removedNodes = [..., <%c${ selector }%c>, ...]` , 'color: red' , '' ) ; cb ( node ) ; } else if ( node . querySelector ( selector ) ) { console . log ( `<${ this . getSelector ( target ) }>.removedNodes = [..., ` + `%c<${ this . getSelector ( node ) }>.querySelector("${ selector }"), ...] ` , 'color: red' ) ; cb ( node . querySelector ( selector ) ) ; } } } } getSelector ( el ) { const classes = Array . from ( el . classList ) ; return el . tagName . toLowerCase ( ) + ( classes . length ? '.' + classes . join ( '.' ) : '' ) + ( el . id . length ? '#' + el . id : '' ) ; } onMutation ( mutations ) { for ( const mutation of mutations ) { const type = pages . getType ( ) ; let userInfo ; let suggestions ; if ( type === PageType . Video ) { let player , skipContainer , overlaySkipButton , subscribeChange ; if ( userInfo = this . isPolyUserInfo ( mutation ) ) { pages . video . setParentNode ( userInfo ) ; pages . video . updatePage ( ) ; } else if ( this . isRelatedUpdate ( mutation ) ) { this . pollUpdate ( pages . video . updateVideos ) ; } else if ( this . isFirstPageLoad ( mutation ) ) { if ( ! pages . video . channelId ) { pages . video . updatePage ( ) ; } } else if ( suggestions = this . isNextVideo ( mutation ) ) { console . log ( mutation ) ; pages . video . onEndscreen ( suggestions ) ; } else if ( player = this . isPlayerUpdate ( mutation ) ) { pages . video . updateAdPlaying ( player , player . classList . contains ( 'ad-showing' ) ) ; let errorState = this . isPlayerErrorChange ( mutation ) ; if ( errorState !== null ) { pages . video . onVideoError ( errorState ) ; } } else if ( skipContainer = this . isAdSkipContainer ( mutation ) ) { pages . video . skipButtonUpdate ( MutationWatcher . adSkipButton ( skipContainer ) ) ; } else if ( overlaySkipButton = this . isOverlayAd ( mutation ) ) { if ( settings . skipOverlays ) overlaySkipButton . click ( ) ; } else if ( ( subscribeChange = this . isSubscribeBtn ( mutation ) ) !== undefined ) { pages . video . updatePage ( ) ; } } else if ( type === PageType . Subscriptions ) { if ( this . isChannelGrid ( mutation ) ) { pages . subscriptions . loadedChannels ( ) ; } } else { if ( type === PageType . Channel ) { let player , skipContainer , subscribeChange ; if ( player = this . isPlayerUpdate ( mutation ) ) { pages . channel . updateAdPlaying ( player , ! ! player . classList . contains ( 'ad-showing' ) ) ; let errorState = this . isPlayerErrorChange ( mutation ) ; if ( errorState !== null ) { pages . channel . onVideoError ( errorState ) ; } } else if ( skipContainer = this . isAdSkipContainer ( mutation ) ) { pages . channel . skipButtonUpdate ( MutationWatcher . adSkipButton ( skipContainer ) ) ; } else if ( ( subscribeChange = this . isSubscribeBtn ( mutation ) ) !== undefined ) { pages . channel . updatePage ( ) ; } } if ( userInfo = this . isPolyUserInfo ( mutation ) ) { pages . video . setParentNode ( userInfo ) ; pages . video . updatePage ( ) ; } if ( this . hasNewItems ( mutation ) ) { if ( type === PageType . Channel ) { this . pollUpdate ( pages . channel . updatePage ) ; } else if ( type === PageType . Search ) { this . pollUpdate ( pages . search . updatePage ) ; } else if ( type === PageType . Any ) { pages . updateAllVideos ( ) ; } } } } } destroy ( ) { this . watcher . disconnect ( ) ; } } class WhitelistButton { toggled : boolean ; button : HTMLButtonElement ; buttonContainer : HTMLDivElement ; constructor ( onClick , toggled ) { this . toggled = toggled ; this . button = document . createElement ( 'button' ) ; this . button . className = 'UBO-wl-btn' ; this . button . title = i18n ( 'whitelistTooltip' ) ; this . button . addEventListener ( 'click' , onClick ) ; this . buttonContainer = document . createElement ( 'div' ) ; this . buttonContainer . className = 'UBO-wl-container' ; } off ( ) { if ( ! this . toggled ) return ; this . toggled = false ; this . button . title = i18n ( 'whitelistTooltip' ) ; this . button . classList . remove ( 'yt-uix-button-toggled' ) ; } on ( ) { if ( this . toggled ) return ; this . toggled = true ; this . button . title = i18n ( 'whitelistedTooltip' ) ; this . button . classList . add ( 'yt-uix-button-toggled' ) ; } } class WhitelistButtonPoly extends WhitelistButton { constructor ( onClick , toggled ) { super ( onClick , toggled ) ; this . button . className += ' UBO-wl-poly ' + ( toggled ? ' yt-uix-button-toggled' : '' ) ; this . button . appendChild ( AdOptions . generateIcon ( icons . checkcircle ) ) ; this . button . appendChild ( document . createTextNode ( i18n ( 'adsEnableBtn' ) . toUpperCase ( ) ) ) ; this . buttonContainer . appendChild ( this . button ) ; } exists ( ) { return ! ! this . buttonContainer . parentElement ; } render ( ) { return this . buttonContainer ; } } /** Class for dealing with pages with only one channel and thus only one Whitelisting button needed */ class SingleChannelPage { dataNode : HTMLElement ; buttonParent : HTMLElement ; whitelistButton : WhitelistButtonInstance ; adOptions : AdOptions ; channelId ? : Channel ; currentAd : Ad ; adVideoId : string ; firstRun : boolean ; adPlaying : boolean ; adConfirmed : boolean ; videoError : boolean ; awaitingSkip : boolean ; pauseOrigin : boolean ; videoId : string ; adsPlayed : number ; skipButton : HTMLButtonElement ; pageManager : HTMLDivElement ; pageApp : HTMLDivElement ; _currentPlayer : HTMLVideoElement ; removeNet : ( ) => void ; removeHooks : ( ) => void ; constructor ( ButtonFactory ) { this . dataNode = null ; this . buttonParent = null ; this . whitelistButton = new ButtonFactory ( this . toggleWhitelist . bind ( this ) , false ) ; this . adOptions = new AdOptions ( this . addBlacklist . bind ( this ) , this . toggleMute . bind ( this ) , this . attemptSkip . bind ( this ) ) ; this . channelId = null ; this . currentAd = null ; this . firstRun = true ; this . adPlaying = false ; this . adConfirmed = false ; this . awaitingSkip = false ; this . videoError = false ; this . skipButton = null ; this . adsPlayed = 0 ; this . videoId = '' ; this . adVideoId = '' ; this . muteTab ( false ) ; this . onKeyboard = this . onKeyboard . bind ( this ) ; this . removeNet = adblock . onNet ( this . onNet . bind ( this ) ) ; this . removeHooks = ( ) => { } ; document . addEventListener ( 'keyup' , this . onKeyboard ) ; log ( 'uBO-YT-Log' , this ) ; } updatePage ( forceUpdate ? , verify ? ) { if ( ! this . dataNode && ! this . setDataNode ( ) ) { log ( 'uBO-wl' , 'Page update requested, but container not available yet' ) ; return ; } this . channelId = this . getChannelId ( this . dataNode ) ; if ( ! this . channelId ) { log ( 'uBO-wl' , 'Page update requested, but channel ID not available yet' ) ; return ; } const whitelisted = settings . asWl ( this . channelId , this . isSubscribed ( ) ) ; pages . updateURL ( whitelisted , verify ) ; whitelisted ? this . whitelistButton . on ( ) : this . whitelistButton . off ( ) ; if ( verify && whitelisted ) { } if ( ! settings . asWl ( this . channelId , this . isSubscribed ( ) ) && this . adPlaying && ! this . awaitingSkip ) { log ( 'uBO-limit' , 'Ad playing that should not be playing, attempting skip' ) ; this . attemptSkip ( ) ; } if ( ! this . whitelistButton . exists ( ) && this . insertButton ( this . whitelistButton ) ) { if ( this . firstRun ) { if ( settings . forceWhite && whitelisted && adblock . immutableBlock ) { log ( 'uBO-reload' , 'An irreversible change was made before status could be determined. Will reload...' ) ; location . reload ( ) ; } log ( 'uBO-reload' , settings . forceWhite , whitelisted , adblock . immutableBlock ) ; let player = document . querySelector ( '#movie_player' ) as HTMLElement ; if ( player ) { this . updateAdPlaying ( player , ! ! player . classList . contains ( 'ad-showing' ) , true ) ; } this . firstRun = false ; } } this . updateAdButton ( ) ; this . updateVideos ( whitelisted , forceUpdate ) ; } onNet ( url ) { if ( ! this . adPlaying ) return ; if ( url . indexOf ( '/api/stats/ads' ) !== - 1 ) { const [ , adVideoId ] = url . match ( /&ad_v=([^&]+)&/ ) || [ , '' ] ; if ( adVideoId && this . adVideoId !== adVideoId ) { this . adVideoId = adVideoId ; log ( 'uBO-video' , adVideoId , this . getVideoId ( ) ) ; getMetadata ( adVideoId ) . then ( metadata => { log ( 'uBO-Ads' , 'Received metadata' , metadata ) ; return getVideoData ( adVideoId , metadata ) ; } ) . then ( ad => { agent . send ( 'echo-ad' , ad ) ; console . log ( 'uBO-Ads' , 'Found an ad' , ad ) ; } ) . catch ( _err => err ( 'uBO-Ads' , 'Could not get ad details' , _err ) ) ; } } } set currentPlayer ( nextPlayer ) { const checkVideoId = ( ) => { const nextVideoId = this . getVideoId ( ) ; if ( this . videoId !== nextVideoId ) { this . videoId = nextVideoId ; this . adsPlayed = 0 ; } } ; if ( nextPlayer && this . _currentPlayer !== nextPlayer ) { let src = nextPlayer . getAttribute ( 'src' ) ; const fn = ( ) => { if ( isNaN ( nextPlayer . duration ) ) return ; if ( nextPlayer . getAttribute ( 'src' ) !== src ) { src = nextPlayer . getAttribute ( 'src' ) ; checkVideoId ( ) ; log ( 'uBO-limit' , 'Video source:' , src , 'VideoID:' , this . videoId ) ; if ( this . adPlaying ) { this . adsPlayed ++ ; log ( 'uBO-limit' , 'Total Ads:' , this . adsPlayed , 'VideoID:' , this . videoId ) ; } } const overPlayLimit = settings . autoSkip && nextPlayer . currentTime > settings . autoSkipSeconds && nextPlayer . duration > settings . autoSkipSeconds ; const overAdsLimit = settings . limitAds && this . adsPlayed > settings . limitAdsQty ; const shouldAutoSkip = overPlayLimit || overAdsLimit ; if ( this . awaitingSkip ) { log ( 'uBO-limit' , 'Re-attempting skip' ) ; this . forceAhead ( nextPlayer ) ; } else if ( this . adPlaying && shouldAutoSkip ) { log ( 'uBO-limit' , 'Automatically skipping per settings' ) ; if ( overAdsLimit ) { log ( 'uBO-limit' , 'Adblock enabled for remainder of video' ) ; adblock . toggleNet ( true ) ; adblock . togglePrune ( true ) ; } this . attemptSkip ( ) ; } } ; nextPlayer . addEventListener ( 'timeupdate' , fn ) ; nextPlayer . addEventListener ( 'durationchange' , fn ) ; this . removeHooks ( ) ; this . removeHooks = ( ) => { nextPlayer . removeEventListener ( 'timeupdate' , fn ) ; nextPlayer . removeEventListener ( 'durationchange' , fn ) ; } ; this . _currentPlayer = nextPlayer ; } } get currentPlayer ( ) { return this . _currentPlayer ; } updateAdPlaying ( player , playing , firstRun = false ) { if ( playing && ! this . adPlaying ) { let container = player . querySelector ( '.ytp-right-controls' ) ; if ( ! container ) return err ( 'Can\'t find .ytp-right-controls' ) ; let options = this . adOptions . renderButton ( ) ; let menu = this . adOptions . renderMenu ( ) ; if ( ! container . contains ( options ) ) { container . insertBefore ( options , container . firstChild ) ; } if ( ! player . contains ( menu ) ) { player . appendChild ( menu ) ; } if ( this . currentPlayer = player . querySelector ( 'video' ) ) { this . adOptions . skipOption = true ; this . adOptions . show ( ) ; if ( settings . autoSkip ) { } if ( adblock . enabled ( ) && this . channelId && ! settings . asWl ( this . channelId , this . isSubscribed ( ) ) ) { this . adPlaying = true ; log ( 'uBO-Ads' , 'Skipping ad due to adblock being enabled and channel not passing the whitelist test' ) ; this . attemptSkip ( ) ; } this . onVideoPlayable ( this . currentPlayer ) . then ( ( ) => this . updateAdButton ( ) ) ; } if ( firstRun ) { agent . send ( 'recent-ad' ) . then ( message => { this . currentAd = message . response as Ad ; this . updateAdButton ( ) ; } ) ; const skipContainer = player . querySelector ( ".ytp-ad-skip-button-container" ) as HTMLElement ; if ( skipContainer ) { const skipButton = MutationWatcher . adSkipButton ( skipContainer ) ; if ( skipButton ) this . skipButtonUpdate ( skipButton ) ; const adContainer = skipContainer . closest ( '.ytp-ad-module' ) ; if ( adContainer ) { adContainer . classList . remove ( 'video-ads' ) ; adContainer . classList . replace ( 'ytp-ad-module' , 'ytp-ubo-ad-module' ) ; } } } this . adPlaying = true ; } else if ( ! playing && this . adPlaying ) { this . adOptions . hide ( ) ; this . adOptions . reset ( ) ; this . adPlaying = false ; this . adConfirmed = false ; this . awaitingSkip = false ; this . skipButton = null ; this . currentAd = null ; if ( this . shouldPause ( ) ) { this . schedulePause ( ) ; } else { this . muteTab ( false ) ; } } if ( this . adPlaying ) { this . updateAdButton ( ) ; } } onKeyboard ( event ) { if ( ! settings . keyboardSkip ) return ; if ( event . key === 'ArrowRight' && this . adPlaying && ! this . awaitingSkip && ! event . composedPath ( ) . find ( ( node ) => node instanceof HTMLElement && ( node . tagName === 'TEXTAREA' || node . tagName === 'INPUT' || node . getAttribute ( 'contenteditable' ) ) ) ) { this . attemptSkip ( ) ; } } onEndscreen ( links ) { log ( 'uBO-suggestions' , links ) ; this . setPageManager ( ) ; links . forEach ( link => { const videoId = ( link . href . match ( /[\?\&]v=([^&]+)/ ) || [ , '' ] ) [ 1 ] ; if ( ! videoId ) throw 'Could not get videoID for' + link . href ; getMetadata ( videoId ) . then ( metadata => getVideoData ( videoId , metadata ) ) . then ( ( { channelId } ) => { link . href = pages . reflectURLFlag ( link . href , settings . asWl ( channelId , false ) ) ; awaitData ( this . pageApp ) . then ( data => { const renderers = Obj . findParent ( data , 'endscreenRenderer' ) ?. endscreenRenderer ?. elements ; renderers . find ( ( renderer ) => renderer . endscreenElementRenderer . endpoint . watchEndpoint ?. videoId === videoId ) . endscreenElementRenderer . endpoint . commandMetadata . webCommandMetadata . url += ( settings . asWl ( channelId ) ? '&disableadblock=1' : '' ) ; } ) . catch ( err => console . error ( 'fuck' , err ) ) ; } ) ; } ) ; } onVideoError ( error ) { this . videoError = error ; if ( this . videoError && this . adPlaying && this . skipButton && settings . skipAdErrors ) { log ( 'uBO-Ads' , 'Skipping ad due to ad failing to load' ) ; this . attemptSkip ( false ) ; } } updateAdInformation ( ad ) { if ( this . currentAd && this . currentAd . video_id !== ad . video_id ) { this . adConfirmed = false ; this . adOptions . reset ( ) ; this . adOptions . skipOption = true ; } this . currentAd = ad ; if ( this . currentPlayer ) { this . onVideoPlayable ( this . currentPlayer ) . then ( ( ) => this . updateAdButton ( ) ) ; } } schedulePause ( ) { let pageTitle ; let intervalId ; let titleChanges = 0 ; this . onVideoPlayable ( this . currentPlayer ) . then ( ( ) => { this . currentPlayer . pause ( ) ; this . muteTab ( false ) ; this . pauseOrigin = true ; pageTitle = document . title ; intervalId = setInterval ( ( ) => { document . title = ++ titleChanges % 2 ? "[❚❚] " + pageTitle : "[❚❚]" ; } , 800 ) ; agent . send ( 'highlight-tab' ) ; return pages . onPageFocus ( ) ; } ) . then ( ( ) => { clearInterval ( intervalId ) ; document . title = pageTitle ; this . currentPlayer . play ( ) ; this . pauseOrigin = false ; } ) ; } shouldPause ( ) { return this . adOptions . muted && settings . pauseAfterAd && document . hidden && ! this . awaitingSkip ; } updateAdButton ( ) { if ( ! this . adConfirmed && this . adPlaying && this . currentAd && this . verifyAd ( ) ) { this . adConfirmed = true ; this . adOptions . muteOption = true ; this . adOptions . blacklistOption = true ; this . adOptions . advertiserName = this . currentAd . channelId . display ; this . adOptions . show ( ) ; } if ( this . adConfirmed ) { const inMutelist = settings . muted . has ( this . currentAd . channelId ) ; const muteAll = ! ! settings . muteAll ; this . muteTab ( muteAll !== inMutelist ) ; if ( settings . blacklisted . has ( this . currentAd . channelId ) ) { log ( 'uBO-Ads' , 'Skipping ad due to advertiser being in blacklist' ) ; this . attemptSkip ( ) ; } } else if ( this . adPlaying && this . currentPlayer ) { this . muteTab ( settings . muteAll ) ; } } attemptSkip ( mute = true ) { if ( ! this . currentPlayer || ! this . adPlaying ) return ; if ( this . skipButton ) { return this . skipButton . click ( ) ; } this . awaitingSkip = true ; this . muteTab ( mute ) ; this . forceAhead ( this . currentPlayer ) ; this . currentPlayer . playbackRate = 5 ; } forceAhead ( player ) { const limit = this . getPlaybackLimit ( player ) ; if ( isNaN ( limit ) ) { this . onVideoPlayable ( player , false ) . then ( ( ) => { player . currentTime = this . getPlaybackLimit ( player ) - 1 ; } ) . catch ( ( ) => err ( 'Src no longer matches' ) ) ; } else { const target = limit - 1 ; if ( player . currentTime < target ) player . currentTime = target ; } } getPlaybackLimit ( video ) { return video . duration ; } skipButtonUpdate ( skipButton ) { this . skipButton = skipButton as HTMLButtonElement ; const shouldSkip = this . awaitingSkip || ( this . videoError && settings . skipAdErrors ) ; if ( this . skipButton && shouldSkip ) { this . skipButton . click ( ) ; } } verifyAd ( ) { return this . matchAdCompanion ( ) || ( typeof this . currentPlayer . src === 'string' && this . currentPlayer . src . indexOf ( 'blob:' ) === 0 ) ; } onVideoPlayable ( video , resolveAnySrc = true ) { if ( ! ! ( video . currentTime > 0 && ! video . paused && ! video . ended && video . readyState > 2 ) ) return Promise . resolve ( ) ; const lastSrc = video . src ; return new Promise ( ( resolve , reject ) => { const listener = ( ) => { video . removeEventListener ( 'playing' , listener ) ; if ( ! resolveAnySrc && video . src !== lastSrc ) return reject ( ) ; resolve ( ) ; } ; video . addEventListener ( 'playing' , listener ) ; setTimeout ( ( ) => { reject ( ) ; listener ( ) ; } , 4000 ) ; } ) ; } matchAdCompanion ( ) { const companion = document . querySelector ( 'ytd-companion-slot-renderer' ) ; return companion && Obj . get ( companion , 'data.actionCompanionAdRenderer.adVideoId' ) === this . currentAd . video_id ; } addBlacklist ( ) { if ( ! this . currentAd . channelId ) throw ( 'Channel ID not available for blacklisting' ) ; agent . send ( 'set-settings' , { param : this . currentAd . channelId , type : 'add-black' } ) . then ( ( ) => this . attemptSkip ( ) ) . catch ( error => err ( 'Error blacklisting:' , error ) ) ; } muteTab ( shouldMute ) { if ( shouldMute ) { log ( 'uBO-mute' , 'Requesting tab mute' ) ; agent . send ( 'mute-tab' , true ) . then ( resp => this . adOptions . muted = true ) . catch ( error => { err ( 'Error muting:' , error ) ; } ) ; } else { log ( 'uBO-mute' , 'Requesting tab unmute' ) ; const done = ( ) => this . adOptions . muted = false ; agent . send ( 'mute-tab' , false ) . then ( done ) . catch ( done ) ; } } toggleMute ( ) { if ( ! this . currentAd . channelId ) throw 'Ad channel ID not available for muting' ; const shouldMute = ! settings . muted . has ( this . currentAd . channelId ) ; const action = shouldMute ? settings . muted . add ( this . currentAd . channelId ) : settings . muted . remove ( this . currentAd . channelId ) ; action . then ( ( ) => agent . send ( 'mute-tab' , shouldMute ) ) . catch ( ( error ) => err ( 'Error setting settings' , error ) ) ; } toggleWhitelist ( ) { this . channelId = this . getChannelId ( this . dataNode ) ; if ( ! this . channelId ) throw 'Channel ID not available' ; if ( settings . toggleWl ( this . channelId , this . isSubscribed ( ) ) ) { this . whitelistButton . on ( ) ; } else { this . whitelistButton . off ( ) ; } } setPageManager ( ) { this . pageManager = this . pageManager = this . pageManager || document . querySelector ( 'ytd-page-manager' ) ; this . pageApp = this . pageApp || document . querySelector ( 'ytd-app' ) ; return this . pageManager ; } setDataNode ( node ? ) { return node ; } insertButton ( button ) { return false ; } updateVideos ( whitelisted , forceUpdate ) { } getChannelId ( node ) { return Channels . empty ( ) ; } getVideoId ( ) { return '' ; } isSubscribed ( ) { return false ; } ; } class VideoPagePoly extends SingleChannelPage { secondaryDataNode : HTMLElement ; constructor ( ) { super ( WhitelistButtonPoly ) ; this . toggleWhitelist = this . toggleWhitelist . bind ( this ) ; this . updatePage = this . updatePage . bind ( this ) ; this . updateVideos = this . updateVideos . bind ( this ) ; } setDataNode ( container ? ) { this . secondaryDataNode = document . querySelector ( 'ytd-video-secondary-info-renderer' ) ; return this . dataNode = container || this . dataNode || document . querySelector ( 'ytd-app' ) ; } setParentNode ( parent ? ) { this . setDataNode ( ) ; return this . buttonParent = parent || this . buttonParent || document . querySelector ( '#top-row ytd-video-owner-renderer' ) ; } insertButton ( button ) { this . setParentNode ( ) ; if ( ! this . buttonParent ) { err ( 'uBO-wl' , 'Tried to add WL button, but target parent not found' ) ; return false ; } if ( this . buttonParent . nextSibling ) { this . buttonParent . parentElement . insertBefore ( button . render ( ) , this . buttonParent . nextSibling ) ; } else { this . buttonParent . appendChild ( button . render ( ) ) ; } return true ; } updateVideos ( whitelisted , forceUpdate ) { this . updateInfobar ( this . buttonParent , whitelisted ) ; let relatedVideos = document . querySelectorAll ( 'ytd-compact-video-renderer,ytd-playlist-panel-video-renderer' ) as NodeListOf < VideoPoly > ; pages . updateVideos ( relatedVideos , forceUpdate ) ; } updateInfobar ( container , whitelisted , channelId = this . channelId ) { container = this . setParentNode ( container ) ; if ( ! container ) return false ; if ( ! channelId ) return false ; let links = container . querySelectorAll ( 'a' ) as NodeListOf < InfoLink > ; for ( let link of links ) { if ( ! link . href ) continue ; if ( ! link . channelId || link . channelId !== channelId . id || link . whitelisted !== whitelisted || link . sethref !== link . href ) { link . href = link . sethref = pages . reflectURLFlag ( link . href , whitelisted ) ; link . whitelisted = whitelisted ; link . channelId = channelId . id ; } } } isSubscribed ( ) { if ( ! this . secondaryDataNode ) return false ; return Obj . get ( this . secondaryDataNode , 'data.subscribeButton.subscribeButtonRenderer.subscribed' ) || false ; } getVideoId ( ) { this . setPageManager ( ) ; if ( ! this . pageManager ) { err ( 'uBO-Limit' , 'Could not find page manager' ) ; return '' ; } return Obj . get ( this . pageManager , 'data.playerResponse.videoDetails.videoId' ) || '' ; } getChannelId ( container ? ) { let channelId = Channels . empty ( ) ; container = this . setDataNode ( container ) ; if ( ! container ) return null ; const prevId = this . channelId ? Channels . valid ( this . channelId ) : null ; channelId . id = Obj . get ( container , 'data.playerResponse.videoDetails.channelId' ) || ( ! prevId ? ( window as any ) . ytplayer ?. config ?. args ?. ucid : null ) ; channelId . display = Obj . get ( container , 'data.playerResponse.videoDetails.author' ) || ( ! prevId ? ( window as any ) . ytplayer ?. config ?. args ?. author : null ) ; return Channels . valid ( channelId ) ; } } class ChannelPagePoly extends SingleChannelPage { constructor ( ) { super ( WhitelistButtonPoly ) ; this . toggleWhitelist = this . toggleWhitelist . bind ( this ) ; this . updatePage = this . updatePage . bind ( this ) ; this . updateVideos = this . updateVideos . bind ( this ) ; } setDataNode ( container ) { return this . dataNode = container || this . dataNode || document . querySelector ( 'ytd-page-manager' ) ; } setParentNode ( parent ? ) { return this . buttonParent = parent || this . buttonParent || document . querySelector ( '#edit-buttons' ) ; } insertButton ( button ) { this . setParentNode ( ) ; if ( ! this . buttonParent ) { err ( 'uBO-wl' , 'Tried to add WL button, but target parent not found' ) ; return false ; } this . buttonParent . appendChild ( button . render ( ) ) ; return true ; } updateVideos ( whitelisted , forceUpdate ) { pages . updateAllVideos ( forceUpdate , this . channelId ) ; } getChannelId ( container ) { let channelId = Channels . empty ( ) ; container = this . setDataNode ( container ) ; if ( ! container ) return null ; channelId . username = Obj . get ( container , 'data.response.metadata.channelMetadataRenderer.doubleclickTrackingUsername' ) || '' ; channelId . display = Obj . get ( container , 'data.response.metadata.channelMetadataRenderer.title' ) || '' ; channelId . id = Obj . get ( container , 'data.response.metadata.channelMetadataRenderer.externalId' ) || '' ; return Channels . valid ( channelId ) ; } getVideoId ( ) { this . setPageManager ( ) ; if ( ! this . pageManager || ! ( this . pageManager as any ) . data ) return '' ; const parent = Obj . findParent ( ( this . pageManager as any ) . data , 'channelVideoPlayerRenderer' ) ; if ( ! parent ) { err ( 'uBO-Limit' , 'Could not find current video ID owner under page-manager' ) ; return '' ; } return parent . channelVideoPlayerRenderer . videoId ; } isSubscribed ( ) { return Obj . get ( this . dataNode , 'data.response.header.c4TabbedHeaderRenderer.subscribeButton.subscribeButtonRenderer.subscribed' ) || false ; } } class SearchPagePoly { constructor ( ) { this . updatePage = this . updatePage . bind ( this ) ; } updatePage ( forceUpdate ? ) { let channelElements = document . querySelectorAll ( 'ytd-channel-renderer' ) ; if ( ! channelElements ) return ; for ( let channelElement of channelElements ) { let channelId = this . getChannelId ( channelElement ) ; let whitelisted = settings . asWl ( channelId , this . isSubscribed ( channelElement ) ) ; if ( channelElement . whitelistButton && channelElement . whitelistButton . exists ( ) ) { if ( forceUpdate ) whitelisted ? channelElement . whitelistButton . on ( ) : channelElement . whitelistButton . off ( ) ; } else { let button = new WhitelistButtonPoly ( this . toggleWhitelist . bind ( this , channelElement ) , whitelisted ) ; let container = channelElement . querySelector ( '#subscribe-button' ) ; container . insertBefore ( button . render ( ) , container . firstChild ) ; channelElement . whitelistButton = button ; } } pages . updateAllVideos ( forceUpdate ) ; } toggleWhitelist ( dataNode ) { let channelId = this . getChannelId ( dataNode ) ; if ( ! channelId ) throw 'Channel ID not available' ; settings . toggleWl ( channelId , this . isSubscribed ( dataNode ) ) ; } isSubscribed ( dataNode ) { return Obj . get ( dataNode , 'data.subscriptionButton.subscribed' ) || false ; } getChannelId ( container ) { let channelId = Channels . empty ( ) ; if ( ! container ) throw 'Search element required to get channelId under search mode' ; channelId . display = Obj . get ( container , 'data.title.simpleText' ) || '' ; channelId . id = Obj . get ( container , 'data.channelId' ) || '' ; channelId . username = Channels . fromURL ( Obj . get ( container , 'data.longBylineText.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl' ) ) || '' ; return Channels . valid ( channelId ) ; } } class ChannelFeedPoly { capture : boolean ; container : HTMLElement ; timeout : number ; debug : boolean ; constructor ( ) { this . capture = false ; } updatePage ( force = false ) { if ( this . capture ) return ; if ( location . href . indexOf ( '?uBO-YT-extract' ) !== - 1 ) { this . debug = location . href . indexOf ( '&debug=true' ) !== - 1 ; log ( 'Capturing channels...' ) ; this . insertUnload ( ) ; this . insertCSS ( ) ; this . instantAnimation ( ) ; this . capture = true ; } } insertUnload ( ) { window . addEventListener ( 'beforeunload' , ( ) => { if ( ! this . capture ) { settings . whitelisted . suggest ( undefined ) ; } } ) ; } insertCSS ( ) { const nextCSS = document . createElement ( 'style' ) ; nextCSS . textContent = '#grid-container { display: none !important }' ; document . documentElement . appendChild ( nextCSS ) ; } instantAnimation ( ) { const origRAF = window . requestAnimationFrame ; ( window as any ) . requestAnimationFrame = function ( fn ) { fn ( performance . now ( ) ) ; return Math . random ( ) ; } ; return ( ) => { window . requestAnimationFrame = origRAF ; } ; } loadedChannels ( ) { if ( ! this . capture ) return ; const grids = document . querySelectorAll ( '#grid-container' ) ; if ( ! grids ) { log ( 'YT-uBO-Channels' , 'Could not find grid containers' ) ; if ( ! this . debug ) settings . whitelisted . suggest ( undefined ) ; return ; } if ( this . timeout ) { clearTimeout ( this . timeout ) ; this . timeout = 0 ; } if ( ! this . container ) { this . container = document . querySelector ( 'ytd-section-list-renderer' ) ; if ( ! this . container ) { log ( 'uBO-YT-Channels' , 'Found grids but parent container likely changed. No way to gauge page length' ) ; if ( ! this . debug ) settings . whitelisted . suggest ( undefined ) ; return ; } } const continuations = Obj . get ( this . container , 'data.continuations' ) || [ ] ; if ( continuations . length ) { log ( 'Fetching next page..' ) ; this . timeout = setTimeout ( ( ) => { log ( 'uBO-YT-Channels' , 'Timed out' ) ; if ( ! this . debug ) settings . whitelisted . suggest ( undefined ) ; } , 4000 ) ; document . querySelectorAll ( 'yt-next-continuation' ) . forEach ( el => { console . log ( el ) ; ( el as any ) . data . autoloadEnabled = true ; ( el as any ) . data . autoloadImmediately = true ; ( el as any ) . maybeTriggerAutoload ( ) ; } ) ; window . dispatchEvent ( new UIEvent ( 'resize' ) ) ; } else { const channels = Array . from ( document . querySelectorAll ( 'ytd-channel-renderer' ) ) . map ( el => this . getChannelId ( el as HTMLElement ) ) . filter ( channelId => channelId ) ; log ( 'Collected ' + channels . length + ' channels. Sending them to background' ) ; settings . whitelisted . suggest ( channels ) ; } } getChannelId ( container ) { const channelId = Channels . empty ( ) ; if ( ! container ) throw 'Channel element required to get channelId' ; channelId . display = Obj . get ( container , 'data.longBylineText.runs[0].text' ) ; channelId . username = Channels . fromURL ( Obj . get ( container , 'data.longBylineText.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl' ) ) ; channelId . id = Obj . get ( container , 'data.longBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId' ) ; return Channels . valid ( channelId ) ; } } class Channels { private type : string ; private list : Array < Channel > ; constructor ( list , type ) { this . list = list ; this . type = type ; } static empty ( ) { return { id : '' , username : '' , display : '' } ; } static valid ( channel ) { return ( channel && ( channel . id || channel . username ) ) ? channel : null ; } static fromLinks ( links ) { const channel = Channels . empty ( ) ; for ( const link of links ) { if ( ! link . href ) continue ; const matches = link . href . match ( /\/(user|channel)\/([\w-]+)(?:\/|$|\?)/ ) ; if ( ! matches ) continue ; const [ _ , type , id ] = matches ; if ( type === 'user' ) channel . username = id ; else if ( type === 'channel' ) { channel . id = id ; if ( link . textContent ) channel . display = link . textContent ; } } return channel ; } static fromURL ( url ) { if ( ! url ) return '' ; const matches = url . match ( /\/user\/(.+)/ ) ; if ( matches && matches . length > 1 ) { return matches [ 1 ] ; } else { return '' ; } } has ( channel ) { if ( ! channel ) return false ; const needle = typeof channel === 'string' ? channel : channel . id ; return this . list . findIndex ( ( { id } ) => id === needle ) !== - 1 ; } remove ( channel ) { return agent . send ( 'set-settings' , { param : channel , type : 'remove-' + this . type } ) ; } add ( channel ) { return agent . send ( 'set-settings' , { param : channel , type : 'add-' + this . type } ) ; } suggest ( channels ) { return agent . send ( 'set-settings' , { param : channels , type : 'suggest-' + this . type } ) ; } } class Settings implements _Settings < Channels > { whitelisted : Channels ; muted : Channels ; exclude : Channels ; blacklisted : Channels ; pauseAfterAd : boolean ; autoWhite : boolean ; muteAll : boolean ; skipAdErrors : boolean ; skipOverlays : boolean ; autoSkip : boolean ; autoSkipSeconds : AutoSkipSeconds ; keyboardSkip : boolean ; verifyWl : boolean ; limitAds : boolean ; limitAdsQty : number ; forceWhite : boolean ; constructor ( settings ) { Object . assign ( this , { ... settings , whitelisted : new Channels ( settings . whitelisted , 'white' ) , muted : new Channels ( settings . muted , 'mute' ) , exclude : new Channels ( settings . exclude , 'exclude' ) , blacklisted : new Channels ( settings . blacklisted , 'black' ) } ) ; } /** Determine whether channel should be treated as whitelisted */ asWl ( channel , subscribed = false ) { return settings . whitelisted . has ( channel ) || ( settings . autoWhite && subscribed && ! settings . exclude . has ( channel ) ) ; } /** Toggle a channel's whitelisted status */ toggleWl ( channel , subscribed = false ) { const curWl = this . asWl ( channel , subscribed ) ; if ( curWl ) { if ( this . autoWhite && subscribed ) this . exclude . add ( channel ) ; this . whitelisted . remove ( channel ) ; return false ; } else { if ( this . autoWhite && subscribed ) this . exclude . remove ( channel ) ; else this . whitelisted . add ( channel ) ; return true ; } } } class Page { video : VideoPagePoly ; channel : ChannelPagePoly ; search : SearchPagePoly ; subscriptions : ChannelFeedPoly ; currentURL : string ; videosQuery : string ; mode : number ; eventExemptions : Array < EventListener > ; originalURL : string ; flaggedURL : string ; constructor ( design ) { if ( design === Layout . Polymer ) { this . video = new VideoPagePoly ( ) ; this . channel = new ChannelPagePoly ( ) ; this . search = new SearchPagePoly ( ) ; this . subscriptions = new ChannelFeedPoly ( ) ; } this . currentURL = '' ; this . eventExemptions = [ ] ; this . videosQuery = [ 'ytd-rich-grid-video-renderer' , 'ytd-grid-video-renderer' , 'ytd-video-renderer' , 'ytd-playlist-video-renderer' , 'ytd-rich-item-renderer' ] . join ( ',' ) ; this . updateAllVideos = this . updateAllVideos . bind ( this ) ; } static getDesign ( ) { if ( ( window as any ) . Polymer || document . querySelector ( 'ytd-app' ) ) { return Layout . Polymer ; } else { return Layout . Unknown ; } } getType ( ) { const nextURL = location . href ; if ( nextURL !== this . currentURL ) { if ( this . originalURL === nextURL ) { window . history . replaceState ( history . state , '' , this . flaggedURL ) ; } return this . mode = this . determineType ( nextURL ) ; } else { return this . mode ; } } determineType ( url = location . href ) { if ( url . indexOf ( 'youtube.com/watch?' ) !== - 1 ) { return PageType . Video ; } else if ( url . indexOf ( 'youtube.com/channel/' ) !== - 1 || url . indexOf ( 'youtube.com/user/' ) !== - 1 || url . indexOf ( 'youtube.com/c/' ) !== - 1 ) { return PageType . Channel ; } else if ( url . indexOf ( 'youtube.com/results?' ) !== - 1 ) { toggleAdblock ( true ) ; return PageType . Search ; } else if ( url . indexOf ( 'youtube.com/feed/channels' ) !== - 1 ) { toggleAdblock ( true ) ; return PageType . Subscriptions ; } else { toggleAdblock ( true ) ; return PageType . Any ; } } update ( forceUpdate ? , verify ? ) { let mode = this . getType ( ) ; if ( mode === PageType . Video ) { this . video . updatePage ( forceUpdate , verify ) ; } else if ( mode === PageType . Channel ) { this . channel . updatePage ( forceUpdate , verify ) ; } else if ( mode === PageType . Search ) { this . search . updatePage ( forceUpdate ) ; } else if ( mode === PageType . Subscriptions ) { this . subscriptions . updatePage ( forceUpdate ) ; } else if ( mode === PageType . Any ) { this . updateURL ( false , false ) ; this . updateAllVideos ( forceUpdate ) ; } } updateAd ( ad , mode = this . getType ( ) ) { if ( mode === PageType . Video ) { this . video . updateAdInformation ( ad ) ; } else if ( mode === PageType . Channel ) { this . channel . updateAdInformation ( ad ) ; } } updateVideos ( videos , forceUpdate ? , channelId ? ) { for ( let video of videos ) { if ( ! forceUpdate && video . data . processed ) continue ; let id = Obj . get ( video , 'data.channelId' ) || Obj . get ( video , 'data.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId' ) || Obj . get ( video , 'data.content.videoRenderer.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId' ) || ( channelId && channelId . id ) ; if ( id ) { let links = video . querySelectorAll ( 'a[href^="/watch?"]' ) as NodeListOf < HTMLAnchorElement > ; if ( ! links . length ) continue ; let destURL = video . data . originalHref ; if ( settings . asWl ( id ) ) { if ( ! video . data . originalHref ) { destURL = links [ 0 ] . getAttribute ( 'href' ) ; video . data . originalHref = destURL ; } destURL += '&disableadblock=1' ; } else { if ( ! destURL ) { video . data . processed = true ; continue ; } } for ( let link of links ) link . href = destURL ; if ( Obj . get ( video , 'data.content.videoRenderer.navigationEndpoint.commandMetadata.webCommandMetadata.url' ) ) video . data . content . videoRenderer . navigationEndpoint . commandMetadata . webCommandMetadata . url = destURL ; if ( Obj . get ( video , 'data.navigationEndpoint.webNavigationEndpointData.url' ) ) video . data . navigationEndpoint . webNavigationEndpointData . url = destURL ; if ( Obj . get ( video , 'data.navigationEndpoint.commandMetadata.webCommandMetadata.url' ) ) video . data . navigationEndpoint . commandMetadata . webCommandMetadata . url = destURL ; video . data . processed = true ; } } } updateAllVideos ( forceUpdate ? , channelId ? ) { const videos = document . querySelectorAll ( this . videosQuery ) as NodeListOf < VideoPoly > ; return this . updateVideos ( videos , forceUpdate , channelId ) ; } updateURL ( whitelisted , verify ) { if ( location . href . indexOf ( '&disableadblock=1' ) !== - 1 ) { if ( ! whitelisted ) { this . originalURL = location . href ; this . flaggedURL = pages . reflectURLFlag ( location . href , false ) ; window . history . replaceState ( history . state , '' , this . flaggedURL ) ; toggleAdblock ( true ) ; return false ; } else { toggleAdblock ( false ) ; return true ; } } else { if ( whitelisted ) { this . originalURL = location . href ; this . flaggedURL = pages . reflectURLFlag ( location . href , false ) ; window . history . replaceState ( history . state , '' , this . flaggedURL ) ; toggleAdblock ( false ) ; if ( verify && settings . verifyWl ) this . confirmDisabled ( ) ; return true ; } else { toggleAdblock ( true ) ; return false ; } } } reflectURLFlag ( url , shouldContain ) { let search = /((?!\?)igno=re&disableadblock=1&?)|(&disableadblock=1)/g ; if ( shouldContain ) { url = this . reflectURLFlag ( url , false ) ; let paramsStart = url . indexOf ( '?' ) ; return url + ( paramsStart === - 1 ? '?igno=re' : ( paramsStart === url . length - 1 ? 'igno=re' : '' ) ) + '&disableadblock=1' ; } else { return url . replace ( search , '' ) ; } } confirmDisabled ( ) { setTimeout ( ( ) => fetch ( 'https://www.youtube.com/favicon.ico?ads=true' ) . catch ( ( ) => prompt ( i18n ( 'adsStillBlocked' ) , '*youtube.com/*&disableadblock=1' ) ) , 300 ) ; } onPageFocus ( ) { if ( ! document . hidden ) return Promise . resolve ( ) ; return new Promise ( resolve => { const listener = ( ) => { if ( ! document . hidden ) { document . removeEventListener ( "visibilitychange" , listener ) ; this . eventExemptions = this . eventExemptions . filter ( fn => fn !== listener ) ; resolve ( ) ; } } ; this . eventExemptions = this . eventExemptions . concat ( listener ) ; document . addEventListener ( "visibilitychange" , listener ) ; } ) ; } destroy ( ) { pages . video . removeHooks ( ) ; pages . channel . removeHooks ( ) ; document . removeEventListener ( 'keyup' , this . video . onKeyboard ) ; document . removeEventListener ( 'keyup' , this . channel . onKeyboard ) ; const nodes = document . querySelectorAll ( '.UBO-ads-btn,.UBO-wl-btn,.UBO-wl-container,.UBO-menu' ) ; for ( let node of nodes ) { node . remove ( ) ; } } } const hookToast = ( ) => { const toast = document . querySelector ( "ytd-popup-container paper-toast.yt-notification-action-renderer" ) as HTMLElement ; if ( ! toast ) return ( ) => { } ; const label = toast . querySelector ( "span#label" ) ; return ( text ) => { label . textContent = text ; toast . style . display = "" ; toast . classList . add ( 'paper-toast-open' ) ; setTimeout ( ( ) => { toast . style . display = "none" ; toast . classList . remove ( 'paper-toast-open' ) ; } , 3000 ) ; } ; } ; class LoadHastener { watcher : MutationObserver ; designConfirmed : ( design : Layout ) => void ; constructor ( ) { this . watcher = new MutationObserver ( mutations => { for ( let mutation of mutations ) { if ( mutation . type === 'childList' ) { for ( let node of mutation . addedNodes ) { if ( node . nodeName === 'BODY' ) { return this . switchToBody ( ) ; } else if ( node . nodeName === 'SCRIPT' ) { if ( ( node as HTMLScriptElement ) . src . indexOf ( 'polymer.js' ) !== - 1 ) { return this . confirmDesign ( Layout . Polymer ) ; } } else if ( ( node as Element ) . localName === 'ytd-app' ) { return this . confirmDesign ( Layout . Polymer ) ; } } } } } ) ; this . designConfirmed = null ; this . contentLoaded = this . contentLoaded . bind ( this ) ; } getDesign ( ) { if ( document . readyState === 'complete' || document . readyState === 'interactive' ) { return Promise . resolve ( Page . getDesign ( ) ) ; } else { return new Promise ( resolve => { this . designConfirmed = resolve ; this . watcher . observe ( document . body || document . documentElement , { childList : true } ) ; document . addEventListener ( 'DOMContentLoaded' , this . contentLoaded ) ; } ) ; } } confirmDesign ( design ) { this . watcher . disconnect ( ) ; document . removeEventListener ( 'DOMContentLoaded' , this . contentLoaded ) ; this . designConfirmed ( design ) ; } contentLoaded ( ) { this . confirmDesign ( Page . getDesign ( ) ) ; } switchToBody ( ) { this . watcher . disconnect ( ) ; this . watcher . observe ( document . body , { childList : true } ) ; } } const hookNav = ( onURL ) => { const hookLinks = ( onURL ) => { const listener = ( e ) => { const link = e . composedPath ( ) . find ( ( node ) => node . tagName === 'A' ) as HTMLAnchorElement ; const href = link && link . getAttribute ( 'href' ) ; if ( href ) { onURL ( href ) ; } } ; document . addEventListener ( 'click' , listener , true ) ; return ( ) => { document . removeEventListener ( 'click' , listener , true ) ; } ; } ; const hookHistory = ( onURL ) => { const listener = ( ) => { if ( location . href ) { onURL ( location . href ) ; } } ; window . addEventListener ( 'popstate' , listener ) ; return ( ) => { window . removeEventListener ( 'popstate' , listener ) ; } ; } ; const unhookLinks = hookLinks ( onURL ) ; const unhookHistory = hookHistory ( onURL ) ; return ( ) => { unhookLinks ( ) ; unhookHistory ( ) ; } ; } ; const init = ( design ) => { pages = new Page ( design || Page . getDesign ( ) ) ; watcher = new MutationWatcher ( ) ; pages . update ( true ) ; watcher . start ( ) ; agent . on ( 'settings-update' , ( updated ) => { settings = new Settings ( updated . settings ) ; pages . update ( true , updated . initiator ) ; } ) . on ( 'ad-update' , ( ad ) => { pages . updateAd ( ad ) ; } ) . on ( 'destroy' , ( ) => { log ( 'uBO-YT' , 'Detaching inject script..' ) ; watcher . destroy ( ) ; pages . destroy ( ) ; agent = null ; watcher = null ; pages = null ; } ) . send ( 'ready' ) ; } ; const [ getEventListeners , awaitEventListener , filterEventListeners , unhookEvents ] = hookEvents ( ) ; ( window as any ) . gev = getEventListeners ; const adblock = new AdBlock ( location . href . indexOf ( '&disableadblock=1' ) === - 1 ) ; adblock . onAds ( ads => { log ( 'uBO-Replace' , ads ) ; if ( ads instanceof Array ) { } return ads ; } ) ; const toggleAdblock = ( nextBlock ) => { adblock . toggleAll ( nextBlock ) ; adblock . togglePrune ( nextBlock && ! settings . autoWhite ) ; } ; const unhookLinks = hookNav ( link => toggleAdblock ( link . indexOf ( '&disableadblock=1' ) === - 1 ) ) ; filterEventListeners ( "visibilitychange" , ( target , { fn } ) => pages ? pages . eventExemptions . indexOf ( fn ) !== - 1 : false ) ; agent = new MessageAgent ( 'uBOWL-message' , true ) ; agent . on ( 'destroy' , ( ) => { log ( 'uBO-YT' , 'Detaching all hooks and destroying agent' ) ; unhookEvents ( ) ; adblock . destroy ( ) ; unhookLinks ( ) ; agent . destroy ( ) ; log ( 'uBO-YT' , 'Gone.' ) ; } ) . send ( 'get-settings' ) . then ( response => { settings = new Settings ( response . settings ) ; AdOptions . uboIcon = response . accessURLs . ICO ; seti18n ( response . i18n ) ; let load = new LoadHastener ( ) ; load . getDesign ( ) . then ( design => init ( design ) ) ; } ) ;