/* 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 = ` `; const checkmarkIcon = ` `; const defaultIcon = `minus-thick`; function buildButtonDiv() { let buttonDiv = document.createElement('div'); buttonDiv.setAttribute('id', 'ta-channel-button'); Object.assign(buttonDiv.style, { display: 'flex', alignItems: 'center', backgroundColor: '#00202f', color: '#fff', fontSize: '14px', padding: '5px', margin: '5px', borderRadius: '8px', }); return buttonDiv; } function buildSubLink(channelContainer) { let subLink = document.createElement('span'); subLink.innerText = 'Subscribe'; subLink.addEventListener('click', e => { e.preventDefault(); 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 { subText = channelContainer.querySelector('#text').textContent; } e.target.title = 'TA Subscribe: ' + subText; }); Object.assign(subLink.style, { padding: '5px', cursor: 'pointer', }); return subLink; } function buildSpacer() { let spacer = document.createElement('span'); spacer.innerText = '|'; return spacer; } function buildDlLink(channelContainer) { let dlLink = document.createElement('span'); dlLink.innerHTML = downloadIcon; dlLink.addEventListener('click', e => { e.preventDefault(); let currentLocation = window.location.href; console.log('download: ' + currentLocation); sendUrl(currentLocation, 'download', dlLink); }); 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 dlLink; } function buildChannelButton(channelContainer) { let buttonDiv = buildButtonDiv(); let subLink = buildSubLink(channelContainer); buttonDiv.appendChild(subLink); let spacer = buildSpacer(); buttonDiv.appendChild(spacer); let dlLink = buildDlLink(channelContainer); buttonDiv.appendChild(dlLink); return buttonDiv; } function getChannelContainers() { let nodes = document.querySelectorAll('#inner-header-container, #owner'); return nodes; } function getTitleContainers() { let nodes = document.querySelectorAll('#video-title'); return nodes; } // fix positioning of #owner div to fit new button function adjustOwner(channelContainer) { let sponsorButton = channelContainer.querySelector('#sponsor-button'); if (sponsorButton === null) { return channelContainer; } let variableMinWidth; if (sponsorButton.hasChildNodes()) { variableMinWidth = '140px'; } else { variableMinWidth = '45px'; } Object.assign(channelContainer.firstElementChild.style, { minWidth: variableMinWidth, }); Object.assign(channelContainer.style, { minWidth: 'calc(40% + 50px)', }); return channelContainer; } 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) { if (titleContainer.hasTA) continue; let videoButton = buildVideoButton(titleContainer); if (videoButton == null) continue; processTitle(titleContainer); titleContainer.appendChild(videoButton); titleContainer.hasTA = true; } } function getNearestLink(element) { for (let i = 0; i < 5 && element && element !== document; i++) { if (element.tagName === 'A' && element.getAttribute('href') !== '#') { return element.getAttribute('href'); } element = element.parentNode; } return null; } function processTitle(titleContainer) { Object.assign(titleContainer.style, { display: 'flex', gap: '15px', }); titleContainer.classList.add('title-container'); titleContainer.addEventListener('mouseover', () => { const taButton = titleContainer.querySelector('.ta-button'); if (!taButton) return; if (!taButton.isChecked) checkVideoExists(taButton); taButton.style.opacity = 1; }); titleContainer.addEventListener('mouseout', () => { const taButton = titleContainer.querySelector('.ta-button'); if (!taButton) return; taButton.style.opacity = 0; }); } function checkVideoExists(taButton) { function handleResponse(message) { let buttonSpan = taButton.querySelector('span'); if (message) { buttonSpan.innerHTML = checkmarkIcon; } else { buttonSpan.innerHTML = downloadIcon; } taButton.isChecked = true; } function handleError() { console.log('error'); } let videoId = taButton.dataset.id; let message = { type: 'videoExists', videoId }; let sending = sendMessage(message); sending.then(handleResponse, handleError); } function buildVideoButton(titleContainer) { let href = getNearestLink(titleContainer); const dlButton = document.createElement('a'); dlButton.classList.add('ta-button'); dlButton.href = '#'; let params = new URLSearchParams(href); let videoId = params.get('/watch?v'); if (!videoId) return; dlButton.setAttribute('data-id', videoId); dlButton.setAttribute('data-type', 'video'); dlButton.title = `TA download video ${videoId}`; Object.assign(dlButton.style, { display: 'flex', 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; Object.assign(dlIcon.style, { filter: 'invert()', width: '18px', height: '18px', padding: '7px 8px', }); dlButton.appendChild(dlIcon); dlButton.addEventListener('click', e => { e.preventDefault(); sendDownload(dlButton); e.stopPropagation(); }); return dlButton; } function sendDownload(button) { let url = button.dataset.id; if (!url) return; sendUrl(url, 'download', button); } function buttonError(button) { let buttonSpan = button.querySelector('span'); if (buttonSpan === null) { buttonSpan = button; } buttonSpan.style.filter = 'invert(19%) sepia(93%) saturate(7472%) hue-rotate(359deg) brightness(105%) contrast(113%)'; buttonSpan.style.color = 'red'; button.style.opacity = 1; button.addEventListener('mouseout', () => { Object.assign(button.style, { opacity: 1, }); }); } function buttonSuccess(button) { let buttonSpan = button.querySelector('span'); if (buttonSpan === null) { buttonSpan = button; } if (buttonSpan.innerHTML === 'Subscribe') { buttonSpan.innerHTML = 'Success'; setTimeout(() => { buttonSpan.innerHTML = 'Subscribe'; }, 2000); } else { buttonSpan.innerHTML = checkmarkIcon; } } function sendUrl(url, action, button) { function handleResponse(message) { console.log('sendUrl response: ' + JSON.stringify(message)); if (message === null || message.detail === 'Invalid token.') { buttonError(button); } else { buttonSuccess(button); } } function handleError(error) { console.log('error'); console.log(JSON.stringify(error)); buttonError(button); } let message = { type: action, url }; console.log('youtube link: ' + JSON.stringify(message)); let sending = sendMessage(message); sending.then(handleResponse, handleError); } function cleanButtons() { console.log('trigger clean buttons'); document.querySelectorAll('.ta-button').forEach(button => { button.parentElement.hasTA = false; button.remove(); }); } let oldHref = document.location.href; 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)) { throttle(ensureTALinks, 700); } }); observer.observe(document.body, { attributes: false, childList: true, subtree: true });