/* content script running on youtube.com */ 'use strict'; const downloadIcon = ` `; const checkmarkIcon = ` `; const defaultIcon = `minus-thick`; 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('#inner-header-container, #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) { if (titleContainer.hasTA) continue; let videoButton = buildVideoButton(titleContainer); if (videoButton == null) continue; processTitle(titleContainer); titleContainer.appendChild(videoButton); titleContainer.hasTA = true; } } // fix positioning of #owner div to fit new button function adjustOwner(channelContainer) { return channelContainer.querySelector('#buttons') || channelContainer; } function buildChannelButton(channelContainer) { let channelHandle = getChannelHandle(channelContainer); let buttonDiv = buildChannelButtonDiv(); let channelSubButton = buildChannelSubButton(channelHandle); buttonDiv.appendChild(channelSubButton); let spacer = buildSpacer(); buttonDiv.appendChild(spacer); let channelDownloadButton = buildChannelDownloadButton(); buttonDiv.appendChild(channelDownloadButton); return buttonDiv; } 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 buildChannelButtonDiv() { 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-left': '8px', borderRadius: '18px', }); 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 => { 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); } else { console.log('Unknown state'); } }); Object.assign(channelSubButton.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() { buttonError(channelSubButton); channelSubButton.innerText = 'Error'; console.log('error'); } let channelHandle = channelSubButton.dataset.id; let message = { type: 'getChannel', channelHandle }; let sending = sendMessage(message); sending.then(handleResponse, handleError); } function buildSpacer() { let spacer = document.createElement('span'); spacer.innerText = '|'; 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 => { e.preventDefault(); console.log(`download: ${currentLocation}`); sendDownload(channelDownloadButton); }); Object.assign(channelDownloadButton.style, { filter: 'invert()', width: '20px', padding: '0 5px', cursor: 'pointer', }); return channelDownloadButton; } function getTitleContainers() { let nodes = document.querySelectorAll('#video-title'); return nodes; } function buildVideoButton(titleContainer) { let href = getNearestLink(titleContainer); if (!href) return; const dlButton = document.createElement('a'); dlButton.classList.add('ta-button'); dlButton.href = '#'; 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]; } 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: '15px', height: '15px', padding: '7px 8px', }); dlButton.appendChild(dlIcon); dlButton.addEventListener('click', e => { e.preventDefault(); sendDownload(dlButton); e.stopPropagation(); }); return dlButton; } 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) { if (titleContainer.hasListener) return; 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; }); titleContainer.hasListener = true; } 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() { buttonError(taButton); let videoId = taButton.dataset.id; console.log(`error: failed to get info from TA for video ${videoId}`); } let videoId = taButton.dataset.id; let message = { type: 'videoExists', videoId }; let sending = sendMessage(message); sending.then(handleResponse, handleError); } 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 = 'Unsubscribe'; }, 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); } 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; 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 });