/* 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.classList.add('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(channelHandle) { let subLink = document.createElement('span'); subLink.innerText = 'Subscribe'; subLink.title = `TA Subscribe: ${channelHandle}`; subLink.setAttribute('data-id', channelHandle); subLink.setAttribute('data-type', 'channel'); subLink.addEventListener('click', e => { e.preventDefault(); console.log(`subscribe to: ${channelHandle}`); sendUrl(channelHandle, 'subscribe', subLink); }); subLink.addEventListener('mouseover', () => { checkChannelSubscribed(channelHandle); }); Object.assign(subLink.style, { padding: '5px', cursor: 'pointer', }); return subLink; } function checkChannelSubscribed(channelHandle) { console.log(`check channel subscribed: ${channelHandle}`); } function buildSpacer() { let spacer = document.createElement('span'); spacer.innerText = '|'; return spacer; } function buildDlLink() { let dlLink = document.createElement('span'); let currentLocation = window.location.href; let urlObj = new URL(currentLocation); if (urlObj.pathname.startsWith('/watch')) { let videoId = urlObj.search.split('=')[1]; dlLink.setAttribute('data-type', 'video'); dlLink.setAttribute('data-id', videoId); dlLink.title = `TA download video: ${videoId}`; } else { let toDownload = urlObj.pathname.slice(1); dlLink.setAttribute('data-id', toDownload); dlLink.setAttribute('data-type', 'channel'); dlLink.title = `TA download channel ${toDownload}`; } dlLink.innerHTML = downloadIcon; dlLink.addEventListener('click', e => { e.preventDefault(); console.log(`download: ${currentLocation}`); sendUrl(currentLocation, 'download', dlLink); }); Object.assign(dlLink.style, { filter: 'invert()', width: '20px', padding: '0 5px', cursor: 'pointer', }); return dlLink; } function getChannelHandle(channelContainer) { const channelHandleContainer = document.querySelector('#channel-handle'); let channelHandle = channelHandleContainer ? channelHandleContainer.innerText : null; if (!channelHandle) { let href = channelContainer.querySelector('.ytd-video-owner-renderer').href; const urlObj = new URL(href); channelHandle = urlObj.pathname.split('/')[1]; } return channelHandle; } function buildChannelButton(channelContainer) { let channelHandle = getChannelHandle(channelContainer); let buttonDiv = buildButtonDiv(); let subLink = buildSubLink(channelHandle); buttonDiv.appendChild(subLink); let spacer = buildSpacer(); buttonDiv.appendChild(spacer); let dlLink = buildDlLink(); 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('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; }); } 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: ${titleContainer.innerText} [${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(); }); document.querySelectorAll('.ta-channel-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 });