@ -4,6 +4,32 @@ content script running on youtube.com
'use strict' ;
let browserType = getBrowser ( ) ;
// boilerplate to dedect browser type api
function getBrowser ( ) {
if ( typeof chrome !== 'undefined' ) {
if ( typeof browser !== 'undefined' ) {
console . log ( 'detected firefox' ) ;
return browser ;
} else {
console . log ( 'detected chrome' ) ;
return chrome ;
}
} else {
console . log ( 'failed to dedect browser' ) ;
throw 'browser detection error' ;
}
}
async function sendMessage ( message ) {
let { success , value } = await browserType . runtime . sendMessage ( message ) ;
if ( ! success ) {
throw value ;
}
return value ;
}
const downloadIcon = ` <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox = "0 0 500 500" style = "enable-background:new 0 0 500 500;" xml : space = "preserve" >
< style type = "text/css" >
@ -85,137 +111,10 @@ viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="pres
< / g >
< / s v g > ` ;
const defaultIcon = ` <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>minus-thick</title><path d="M20 14H4V10H20" /></svg> ` ;
let browserType = getBrowser ( ) ;
// boilerplate to dedect browser type api
function getBrowser ( ) {
if ( typeof chrome !== 'undefined' ) {
if ( typeof browser !== 'undefined' ) {
console . log ( 'detected firefox' ) ;
return browser ;
} else {
console . log ( 'detected chrome' ) ;
return chrome ;
}
} else {
console . log ( 'failed to dedect browser' ) ;
throw 'browser detection error' ;
}
}
function getChannelContainers ( ) {
const elements = document . querySelectorAll ( '.yt-flexible-actions-view-model-wiz, #owner' ) ;
const channelContainerNodes = [ ] ;
elements . forEach ( element => {
if ( isElementVisible ( element ) ) {
channelContainerNodes . push ( element ) ;
}
} ) ;
return channelContainerNodes ;
}
function isElementVisible ( element ) {
return element . offsetWidth > 0 || element . offsetHeight > 0 || element . getClientRects ( ) . length > 0 ;
}
function ensureTALinks ( ) {
let channelContainerNodes = getChannelContainers ( ) ;
for ( let channelContainer of channelContainerNodes ) {
channelContainer = adjustOwner ( channelContainer ) ;
if ( channelContainer . hasTA ) continue ;
let channelButton = buildChannelButton ( channelContainer ) ;
channelContainer . appendChild ( channelButton ) ;
channelContainer . hasTA = true ;
}
let titleContainerNodes = getTitleContainers ( ) ;
for ( let titleContainer of titleContainerNodes ) {
let parent = getNearestH3 ( titleContainer ) ;
if ( ! parent ) continue ;
if ( parent . hasTA ) continue ;
let videoButton = buildVideoButton ( titleContainer ) ;
if ( videoButton == null ) continue ;
processTitle ( parent ) ;
parent . appendChild ( videoButton ) ;
parent . hasTA = true ;
}
}
ensureTALinks = throttled ( ensureTALinks , 700 ) ;
function adjustOwner ( channelContainer ) {
return channelContainer . querySelector ( '#buttons' ) || channelContainer ;
}
function buildChannelButton ( channelContainer ) {
let channelHandle = getChannelHandle ( channelContainer ) ;
channelContainer . taDerivedHandle = channelHandle ;
let buttonDiv = buildChannelButtonDiv ( ) ;
let channelSubButton = buildChannelSubButton ( channelHandle ) ;
buttonDiv . appendChild ( channelSubButton ) ;
channelContainer . taSubButton = channelSubButton ;
let spacer = buildSpacer ( ) ;
buttonDiv . appendChild ( spacer ) ;
let channelDownloadButton = buildChannelDownloadButton ( ) ;
buttonDiv . appendChild ( channelDownloadButton ) ;
channelContainer . taDownloadButton = channelDownloadButton ;
if ( ! channelContainer . taObserver ) {
function updateButtonsIfNecessary ( ) {
let newHandle = getChannelHandle ( channelContainer ) ;
if ( channelContainer . taDerivedHandle === newHandle ) return ;
console . log ( ` updating handle from ${ channelContainer . taDerivedHandle } to ${ newHandle } ` ) ;
channelContainer . taDerivedHandle = newHandle ;
let channelSubButton = buildChannelSubButton ( newHandle ) ;
channelContainer . taSubButton . replaceWith ( channelSubButton ) ;
channelContainer . taSubButton = channelSubButton ;
let channelDownloadButton = buildChannelDownloadButton ( ) ;
channelContainer . taDownloadButton . replaceWith ( channelDownloadButton ) ;
channelContainer . taDownloadButton = channelDownloadButton ;
}
channelContainer . taObserver = new MutationObserver ( throttled ( updateButtonsIfNecessary , 100 ) ) ;
channelContainer . taObserver . observe ( channelContainer , {
attributes : true ,
childList : true ,
subtree : true ,
} ) ;
}
return buttonDiv ;
}
function getChannelHandle ( channelContainer ) {
let channelHandle ;
const videoOwnerRenderer = channelContainer . querySelector ( '.ytd-video-owner-renderer' ) ;
if ( ! videoOwnerRenderer ) {
const channelHandleContainer = document . querySelector (
'.yt-content-metadata-view-model-wiz__metadata-text'
) ;
channelHandle = channelHandleContainer ? channelHandleContainer . innerText : null ;
} else {
const href = videoOwnerRenderer . href ;
if ( href ) {
const urlObj = new URL ( href ) ;
channelHandle = urlObj . pathname . split ( '/' ) [ 1 ] ;
}
}
return channelHandle ;
}
function buildChannelButtonDiv ( ) {
function buildButtonDiv ( ) {
let buttonDiv = document . createElement ( 'div' ) ;
buttonDiv . classList . add ( 'ta-channel-button' ) ;
buttonDiv . setAttribute ( 'id' , 'ta-channel-button' ) ;
Object . assign ( buttonDiv . style , {
display : 'flex' ,
alignItems : 'center' ,
@ -223,60 +122,38 @@ function buildChannelButtonDiv() {
color : '#fff' ,
fontSize : '14px' ,
padding : '5px' ,
'margin-left' : '8 px',
borderRadius : ' 1 8px',
margin : '5 px',
borderRadius : ' 8px',
} ) ;
return buttonDiv ;
}
function buildChannelSubButton ( channelHandle ) {
let channelSubButton = document . createElement ( 'span' ) ;
channelSubButton . innerText = 'Checking...' ;
channelSubButton . title = ` TA Subscribe: ${ channelHandle } ` ;
channelSubButton . setAttribute ( 'data-id' , channelHandle ) ;
channelSubButton . setAttribute ( 'data-type' , 'channel' ) ;
channelSubButton . addEventListener ( 'click' , e => {
function buildSubLink ( channelContainer ) {
let subLink = document . createElement ( 'span' ) ;
subLink . innerText = 'Subscribe' ;
subLink . addEventListener ( 'click' , e => {
e . preventDefault ( ) ;
if ( channelSubButton . innerText === 'Subscribe' ) {
console . log ( ` subscribe to: ${ channelHandle } ` ) ;
sendUrl ( channelHandle , 'subscribe' , channelSubButton ) ;
} else if ( channelSubButton . innerText === 'Unsubscribe' ) {
console . log ( ` unsubscribe from: ${ channelHandle } ` ) ;
sendUrl ( channelHandle , 'unsubscribe' , channelSubButton ) ;
let currentLocation = window . location . href ;
console . log ( 'subscribe to: ' + currentLocation ) ;
sendUrl ( currentLocation , 'subscribe' , subLink ) ;
} ) ;
subLink . addEventListener ( 'mouseover' , e => {
let subText ;
if ( window . location . pathname === '/watch' ) {
let currentLocation = window . location . href ;
subText = currentLocation ;
} else {
console . log ( 'Unknown state' ) ;
subText = channelContainer . querySelector ( '#text' ) . textContent ;
}
e . target . title = 'TA Subscribe: ' + subText ;
} ) ;
Object . assign ( channelSubButton . style , {
Object . assign ( subLink . style , {
padding : '5px' ,
cursor : 'pointer' ,
} ) ;
checkChannelSubscribed ( channelSubButton ) ;
return channelSubButton ;
}
function checkChannelSubscribed ( channelSubButton ) {
function handleResponse ( message ) {
if ( ! message || ( typeof message === 'object' && message . channel _subscribed === false ) ) {
channelSubButton . innerText = 'Subscribe' ;
} else if ( typeof message === 'object' && message . channel _subscribed === true ) {
channelSubButton . innerText = 'Unsubscribe' ;
} else {
console . log ( 'Unknown state' ) ;
}
}
function handleError ( e ) {
buttonError ( channelSubButton ) ;
channelSubButton . innerText = 'Error' ;
console . error ( 'error' , e ) ;
}
let channelHandle = channelSubButton . dataset . id ;
let message = { type : 'getChannel' , channelHandle } ;
let sending = sendMessage ( message ) ;
sending . then ( handleResponse , handleError ) ;
return subLink ;
}
function buildSpacer ( ) {
@ -286,210 +163,163 @@ function buildSpacer() {
return spacer ;
}
function buildChannelDownloadButton ( ) {
let channelDownloadButton = document . createElement ( 'span' ) ;
let currentLocation = window . location . href ;
let urlObj = new URL ( currentLocation ) ;
if ( urlObj . pathname . startsWith ( '/watch' ) ) {
let params = new URLSearchParams ( document . location . search ) ;
let videoId = params . get ( 'v' ) ;
channelDownloadButton . setAttribute ( 'data-type' , 'video' ) ;
channelDownloadButton . setAttribute ( 'data-id' , videoId ) ;
channelDownloadButton . title = ` TA download video: ${ videoId } ` ;
checkVideoExists ( channelDownloadButton ) ;
} else {
channelDownloadButton . setAttribute ( 'data-id' , currentLocation ) ;
channelDownloadButton . setAttribute ( 'data-type' , 'channel' ) ;
channelDownloadButton . title = ` TA download channel ${ currentLocation } ` ;
}
channelDownloadButton . innerHTML = downloadIcon ;
channelDownloadButton . addEventListener ( 'click' , e => {
function buildDlLink ( channelContainer ) {
let dlLink = document . createElement ( 'span' ) ;
dlLink . innerHTML = downloadIcon ;
dlLink . addEventListener ( 'click' , e => {
e . preventDefault ( ) ;
console . log ( ` download: ${ currentLocation } ` ) ;
sendDownload ( channelDownloadButton ) ;
let currentLocation = window . location . href ;
console . log ( 'download: ' + currentLocation ) ;
sendUrl ( currentLocation , 'download' , dlLink ) ;
} ) ;
Object . assign ( channelDownloadButton . style , {
dlLink . addEventListener ( 'mouseover' , e => {
let subText ;
let currentLocation = window . location . href ;
if ( window . location . pathname === '/watch' ) {
subText = currentLocation ;
} else {
subText = channelContainer . querySelector ( '#text' ) . textContent + ' ' + currentLocation ;
}
e . target . title = 'TA Download: ' + subText ;
} ) ;
Object . assign ( dlLink . style , {
filter : 'invert()' ,
width : '20px' ,
padding : '0 5px' ,
cursor : 'pointer' ,
} ) ;
return channelDownloadButton ;
return dlLink ;
}
function getTitleContainers ( ) {
let elements = document . querySelectorAll ( '#video-title' ) ;
let videoNodes = [ ] ;
elements . forEach ( element => {
if ( isElementVisible ( element ) ) {
videoNodes . push ( element ) ;
}
} ) ;
return elements ;
}
function buildChannelButton ( channelContainer ) {
let buttonDiv = buildButtonDiv ( ) ;
let subLink = buildSubLink ( channelContainer ) ;
buttonDiv . appendChild ( subLink ) ;
function getVideoId ( titleContainer ) {
if ( ! titleContainer ) return undefined ;
let spacer = buildSpacer ( ) ;
buttonDiv . appendChild ( spacer ) ;
let href = getNearestLink ( title Container) ;
if ( ! href ) return ;
let dlLink = buildDlLink ( channelContainer ) ;
buttonDiv . appendChild ( dlLink ) ;
let videoId ;
if ( href . startsWith ( '/watch?v' ) ) {
let params = new URLSearchParams ( href ) ;
videoId = params . get ( '/watch?v' ) ;
} else if ( href . startsWith ( '/shorts/' ) ) {
videoId = href . split ( '/' ) [ 2 ] ;
}
return videoId ;
return buttonDiv ;
}
function buildVideoButton ( titleContainer ) {
let videoId = getVideoId ( titleContainer ) ;
if ( ! videoId ) return ;
function getChannelContainers ( ) {
let nodes = document . querySelectorAll ( '#inner-header-container, #owner' ) ;
return nodes ;
}
function getThubnailContainers ( ) {
let nodes = document . querySelectorAll ( '#thumbnail' ) ;
return nodes ;
}
function buildVideoButton ( thumbContainer ) {
let thumbLink = thumbContainer ? . href ;
if ( ! thumbLink ) return ;
if ( thumbLink . includes ( 'list=' ) ) return ;
let ggp = thumbContainer ? . parentElement ? . parentElement ;
if ( ggp ? . id !== 'dismissible' ) return ;
const dlButton = document . createElement ( 'a' ) ;
dlButton . classList . add ( 'ta-button' ) ;
le t dlButton = document . createElement ( 'a' ) ;
dlButton . setAttribute( 'id' , 'ta-video -button') ;
dlButton . href = '#' ;
dlButton . addEventListener ( 'click' , e => {
e . preventDefault ( ) ;
let videoLink = thumbContainer . href ;
console . log ( 'download: ' + videoLink ) ;
sendUrl ( videoLink , 'download' , dlButton ) ;
} ) ;
ggp . addEventListener ( 'mouseover' , ( ) => {
Object . assign ( dlButton . style , {
opacity : 1 ,
} ) ;
let videoTitle = thumbContainer . href ;
dlButton . title = 'TA download: ' + videoTitle ;
} ) ;
ggp . addEventListener ( 'mouseout' , ( ) => {
Object . assign ( dlButton . style , {
opacity : 0 ,
} ) ;
} ) ;
Object . assign ( dlButton . style , {
display : 'flex' ,
position : 'absolute' ,
top : '5px' ,
left : '5px' ,
alignItems : 'center' ,
justifyContent : 'center' ,
backgroundColor : '#00202f' ,
color : '#fff' ,
fontSize : '1.4rem' ,
textDecoration : 'none' ,
borderRadius : '8px' ,
cursor : 'pointer' ,
height : 'fit-content' ,
opacity : 0 ,
} ) ;
let dlIcon = document . createElement ( 'span' ) ;
dlIcon . innerHTML = defaultIcon ;
dlIcon . innerHTML = d ownload Icon;
Object . assign ( dlIcon . style , {
filter : 'invert()' ,
width : '15px' ,
height : '15px' ,
width : '18px' ,
padding : '7px 8px' ,
} ) ;
dlButton . appendChild ( dlIcon ) ;
dlButton . addEventListener ( 'click' , e => {
e . preventDefault ( ) ;
sendDownload ( dlButton ) ;
e . stopPropagation ( ) ;
} ) ;
return dlButton ;
}
function getNearestLink ( element ) {
// Check siblings
let sibling = element ;
while ( sibling ) {
sibling = sibling . previousElementSibling ;
if ( sibling && sibling . tagName === 'A' && sibling . getAttribute ( 'href' ) !== '#' ) {
return sibling . getAttribute ( 'href' ) ;
}
}
sibling = element ;
while ( sibling ) {
sibling = sibling . nextElementSibling ;
if ( sibling && sibling . tagName === 'A' && sibling . getAttribute ( 'href' ) !== '#' ) {
return sibling . getAttribute ( 'href' ) ;
}
}
// Check parent elements
for ( let i = 0 ; i < 5 && element && element !== document ; i ++ ) {
if ( element . tagName === 'A' && element . getAttribute ( 'href' ) !== '#' ) {
return element . getAttribute ( 'href' ) ;
}
element = element . parentNode ;
// fix positioning of #owner div to fit new button
function adjustOwner ( channelContainer ) {
let sponsorButton = channelContainer . querySelector ( '#sponsor-button' ) ;
if ( sponsorButton === null ) {
return channelContainer ;
}
return null ;
}
function getNearestH3 ( element ) {
for ( let i = 0 ; i < 5 && element && element !== document ; i ++ ) {
if ( element . tagName === 'H3' ) {
return element ;
}
element = element . parentNode ;
let variableMinWidth ;
if ( sponsorButton . hasChildNodes ( ) ) {
variableMinWidth = '140px' ;
} else {
variableMinWidth = '45px' ;
}
return null ;
}
function processTitle ( titleContainer ) {
if ( titleContainer . hasListener ) return ;
Object . assign ( titleContainer . style , {
display : 'flex' ,
gap : '15px' ,
Object . assign ( channelContainer . firstElementChild . style , {
minWidth : variableMinWidth ,
} ) ;
titleContainer . classList . add ( 'title-container' ) ;
titleContainer . addEventListener ( 'mouseenter' , ( ) => {
const taButton = titleContainer . querySelector ( '.ta-button' ) ;
if ( ! taButton ) return ;
if ( ! taButton . isChecked ) checkVideoExists ( taButton ) ;
taButton . style . opacity = 1 ;
} ) ;
titleContainer . addEventListener ( 'mouseleave' , ( ) => {
const taButton = titleContainer . querySelector ( '.ta-button' ) ;
if ( ! taButton ) return ;
taButton . style . opacity = 0 ;
Object . assign ( channelContainer . style , {
minWidth : 'calc(40% + 50px)' ,
} ) ;
titleContainer . hasListener = true ;
return channelContainer ;
}
function checkVideoExists ( taButton ) {
function handleResponse ( message ) {
let buttonSpan = taButton . querySelector ( 'span' ) || taButton ;
if ( message !== false ) {
buttonSpan . innerHTML = checkmarkIcon ;
buttonSpan . title = 'Open in TA' ;
buttonSpan . addEventListener ( 'click' , ( ) => {
let win = window . open ( message , '_blank' ) ;
win . focus ( ) ;
} ) ;
} else {
buttonSpan . innerHTML = downloadIcon ;
}
taButton . isChecked = true ;
}
function handleError ( e ) {
buttonError ( taButton ) ;
let videoId = taButton . dataset . id ;
console . log ( ` error: failed to get info from TA for video ${ videoId } ` ) ;
console . error ( e ) ;
}
function ensureTALinks ( ) {
let channelContainerNodes = getChannelContainers ( ) ;
let videoId = taButton . dataset . id ;
if ( ! videoId ) {
videoId = getVideoId ( taButton ) ;
if ( videoId ) {
taButton . setAttribute ( 'data-id' , videoId ) ;
taButton . setAttribute ( 'data-type' , 'video' ) ;
taButton . title = ` TA download video: ${ taButton . parentElement . innerText } [ ${ videoId } ] ` ;
}
for ( let channelContainer of channelContainerNodes ) {
channelContainer = adjustOwner ( channelContainer ) ;
if ( channelContainer . hasTA ) continue ;
let channelButton = buildChannelButton ( channelContainer ) ;
channelContainer . appendChild ( channelButton ) ;
channelContainer . hasTA = true ;
}
let message = { type : 'videoExists' , videoId } ;
let sending = sendMessage ( message ) ;
sending . then ( handleResponse , handleError ) ;
}
let thumbContainerNodes = getThubnailContainers ( ) ;
function sendDownload ( button ) {
let url = button . dataset . id ;
if ( ! url ) return ;
sendUrl ( url , 'download' , button ) ;
for ( let thumbContainer of thumbContainerNodes ) {
if ( thumbContainer . hasTA ) continue ;
let videoButton = buildVideoButton ( thumbContainer ) ;
if ( videoButton == null ) continue ;
thumbContainer . parentElement . appendChild ( videoButton ) ;
thumbContainer . hasTA = true ;
}
}
function buttonError ( button ) {
@ -517,10 +347,13 @@ function buttonSuccess(button) {
if ( buttonSpan . innerHTML === 'Subscribe' ) {
buttonSpan . innerHTML = 'Success' ;
setTimeout ( ( ) => {
buttonSpan . innerHTML = ' Uns ubscribe';
buttonSpan . innerHTML = ' S ubscribe';
} , 2000 ) ;
} else {
buttonSpan . innerHTML = checkmarkIcon ;
setTimeout ( ( ) => {
buttonSpan . innerHTML = downloadIcon ;
} , 2000 ) ;
}
}
@ -534,8 +367,9 @@ function sendUrl(url, action, button) {
}
}
function handleError ( e ) {
console . log ( 'error' , e ) ;
function handleError ( error ) {
console . log ( 'error' ) ;
console . log ( JSON . stringify ( error ) ) ;
buttonError ( button ) ;
}
@ -547,50 +381,19 @@ function sendUrl(url, action, button) {
sending . then ( handleResponse , handleError ) ;
}
async function sendMessage ( message ) {
let { success , value } = await browserType . runtime . sendMessage ( message ) ;
if ( ! success ) {
throw value ;
}
return value ;
}
function cleanButtons ( ) {
console . log ( 'trigger clean buttons' ) ;
document . querySelectorAll ( '.ta-button' ) . forEach ( button => {
button . parentElement . hasTA = false ;
button . remove ( ) ;
} ) ;
document . querySelectorAll ( '.ta-channel-button' ) . forEach ( button => {
button . parentElement . hasTA = false ;
button . remove ( ) ;
} ) ;
}
let oldHref = document . location . href ;
function throttled ( callback , time ) {
let throttleBlock = false ;
let lastArgs ;
return ( ... args ) => {
lastArgs = args ;
if ( throttleBlock ) return ;
throttleBlock = true ;
setTimeout ( ( ) => {
throttleBlock = false ;
callback ( ... lastArgs ) ;
} , time ) ;
} ;
}
let throttleBlock ;
const throttle = ( callback , time ) => {
if ( throttleBlock ) return ;
throttleBlock = true ;
setTimeout ( ( ) => {
callback ( ) ;
throttleBlock = false ;
} , time ) ;
} ;
let observer = new MutationObserver ( list => {
const currentHref = document . location . href ;
if ( currentHref !== oldHref ) {
cleanButtons ( ) ;
oldHref = currentHref ;
}
if ( list . some ( i => i . type === 'childList' && i . addedNodes . length > 0 ) ) {
ensureTALinks( ) ;
throttle ( ensureTALinks , 700 ) ;
}
} ) ;