Compare commits

..

No commits in common. 'master' and 'v0.1.3' have entirely different histories.

@ -6,7 +6,6 @@ module.exports = {
},
env: {
browser: true,
es6: true,
},
globals: {
browser: 'readonly',
@ -18,7 +17,5 @@ module.exports = {
eqeqeq: ['error', 'always', { null: 'ignore' }],
curly: ['error', 'multi-line'],
'no-var': 'error',
'no-func-assign': 'off',
'no-inner-declarations': 'off',
},
};

@ -11,11 +11,11 @@ This is a browser extension to bridge YouTube with [Tube Archivist](https://gith
- Add your Tube Archivist connection details in the addon popup.
- On YouTube video pages, inject a download button to download that video and a subscribe button to subscribe to that channel.
- On YouTube channel pages, inject a button to subscribe to the channel or download the complete channel. Regarding the channel subpages, this follows the same rules as adding to the queue over the form.
- Throughout most places, hover over the video title to reveal a download button for that video.
- Throughout most places, hover over the video to reveal a download button for that video.
- Sync your cookies for yt-dlp.
## Screenshots
![popup screenshot](assets/tac-screenshot.png?raw=true "Tube Archivist Companion Popup")
![popup screenshot](assets/screenshot.png?raw=true "Tube Archivist Companion Popup")
Popup to enter your connection details.
<br><br>
@ -23,12 +23,12 @@ Popup to enter your connection details.
Button injected on video page to download the video or subscribe to the channel.
<br><br>
![search page](assets/tac-screenshot-search.jpg?raw=true "Tube Archivist Companion Search Page")
Download button injected showing when hovering over the video title.
![search page](assets/screenshot-search.png?raw=true "Tube Archivist Companion Search Page")
Download button injected showing when hovering over top left corned of thumbnail
<br><br>
![channel page](assets/tac-screenshot-channel.jpg?raw=true "Tube Archivist Companion Channel Page")
Channel button injected to subscribe or download whole channel, video download button showing when hovering over the video title.
![channel page](assets/screenshot-channel.png?raw=true "Tube Archivist Companion Channel Page")
Channel button injected to subscribe or download whole channel, video download button showing when hovering over topleft corner of thumbnail.
<br>
## Install
@ -51,13 +51,9 @@ A green checkmark will appear next to the *Save* button if your connection is wo
## Options
- **Sync YouTube cookies**: Send your cookies to TubeArchivist to use for yt-dlp requests.
- **Autostart**: Autostart and prioritize videos send from this extension.
## Test this extension
Before continuing loading the temporary extension here, make sure to deactivate/delete the main extension first.
Symlink/copy the correct manifest file for your browser to the expected location, e.g. `ln -s manifest-firefox.json manifest.json`.
Use the correct manifest file for your browser. Either rename the browser specific file to `manifest.json` before loading the addon or symlink it to the correct location, e.g. `ln -s manifest-firefox.json manifest.json`.
- Firefox
- Open `about:debugging#/runtime/this-firefox`
- Click on *Load Temporary Add-on*
@ -76,16 +72,12 @@ Symlink/copy the correct manifest file for your browser to the expected location
## Roadmap
Join us on [Discord](https://www.tubearchivist.com/discord) and help us improve and extend this project. This is a list of planned features, in no particular order:
- [ ] Get download and subscribe status from TA to show on the injected buttons
- [ ] Implement download button for videos on the YouTube homepage over inline preview
- [ ] Implement download/subscribe button for playlists
- [ ] Add download buttons to the `/shorts/` pages
- [X] Get download and subscribe status from TA to show on the injected buttons
- [X] Implement download button for videos on the YouTube homepage over inline preview
- [X] Implement download button for videos on playlist
- [X] Error handling for connection errors
- [ ] Implement download button for videos on playlist
- [ ] Error handling for connection errors
- [X] Dynamically inject buttons with mutation observer
## Making changes to the JavaScript
The JavaScript does not require any build step; you just edit the files directly. However, there is config for eslint and prettier (a linter and formatter respectively); their use is recommended but not required. To use them, install `node`, run `npm i` from the root directory of this repository to install dependencies, then run `npm run lint` and `npm run format` to run eslint and prettier respectively.
## Updating Artwork
Google listing is *very* picky. Screenshots need to be exactly **1280x800** in resolution and need to be in *jpg* or *png* without alpha canal.

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

@ -101,13 +101,8 @@ async function verifyConnection() {
// send youtube link from injected buttons
async function download(url) {
let apiURL = 'api/download/';
let autostart = await browserType.storage.local.get('autostart');
if (Object.keys(autostart).length > 0 && autostart.autostart.checked) {
apiURL += '?autostart=true';
}
return await sendData(
apiURL,
'api/download/',
{
data: [
{
@ -120,14 +115,14 @@ async function download(url) {
);
}
async function subscribe(url, subscribed) {
async function subscribe(url) {
return await sendData(
'api/channel/',
{
data: [
{
channel_id: url,
channel_subscribed: subscribed,
channel_subscribed: true,
},
],
},
@ -135,55 +130,6 @@ async function subscribe(url, subscribed) {
);
}
async function videoExists(id) {
const path = `api/video/${id}/`;
let response = await sendGet(path);
if (!response.data) return false;
let access = await getAccess();
return new URL(`video/${id}/`, `${access.url}:${access.port}/`).href;
}
async function getChannelCache() {
let cache = await browserType.storage.local.get('cache');
if (cache.cache) return cache;
return { cache: {} };
}
async function setChannel(channelHandler, channelId) {
let cache = await getChannelCache();
cache.cache[channelHandler] = { id: channelId, timestamp: Date.now() };
browserType.storage.local.set(cache);
}
async function getChannelId(channelHandle) {
let cache = await getChannelCache();
if (cache.cache[channelHandle]) {
return cache.cache[channelHandle]?.id;
}
let channel = await searchChannel(channelHandle);
if (channel) setChannel(channelHandle, channel.channel_id);
return channel.channel_id;
}
async function searchChannel(channelHandle) {
const path = `api/channel/search/?q=${channelHandle}`;
let response = await sendGet(path);
return response.data;
}
async function getChannel(channelHandle) {
let channelId = await getChannelId(channelHandle);
if (!channelId) return;
const path = `api/channel/${channelId}/`;
let response = await sendGet(path);
return response.data;
}
async function cookieStr(cookieLines) {
const path = 'api/cookie/';
let payload = {
@ -208,7 +154,6 @@ function buildCookieLine(cookie) {
async function sendCookies() {
console.log('function sendCookies');
const acceptableDomains = ['.youtube.com', 'youtube.com', 'www.youtube.com'];
let cookieStores = await browserType.cookies.getAllCookieStores();
let cookieLines = [
@ -224,9 +169,7 @@ async function sendCookies() {
});
for (let j = 0; j < allCookiesStore.length; j++) {
const cookie = allCookiesStore[j];
if (acceptableDomains.includes(cookie.domain)) {
cookieLines.push(buildCookieLine(cookie));
}
cookieLines.push(buildCookieLine(cookie));
}
}
@ -244,9 +187,6 @@ type Message =
| { type: 'sendCookie' }
| { type: 'download', url: string }
| { type: 'subscribe', url: string }
| { type: 'unsubscribe', url: string }
| { type: 'videoExists', id: string }
| { type: 'getChannel', url: string }
*/
function handleMessage(request, sender, sendResponse) {
console.log('message background.js listener got message', request);
@ -269,17 +209,7 @@ function handleMessage(request, sender, sendResponse) {
return await download(request.url);
}
case 'subscribe': {
return await subscribe(request.url, true);
}
case 'unsubscribe': {
let channelId = await getChannelId(request.url);
return await subscribe(channelId, false);
}
case 'videoExists': {
return await videoExists(request.videoId);
}
case 'getChannel': {
return await getChannel(request.channelHandle);
return await subscribe(request.url);
}
default: {
let err = new Error(`unknown message type ${JSON.stringify(request.type)}`);

@ -14,7 +14,7 @@
<a href="#" id="ta-url" target="_blank">
<img src="/images/logo.png" alt="ta-logo">
</a>
<span>v0.3.1</span>
<span>v0.1.3</span>
</div>
<hr>
<form class="login-form">
@ -30,14 +30,8 @@
<hr>
<p>Options:</p>
<div class="options">
<div>
<input type="checkbox" id="sendCookies" name="sendCookies">
<span>Sync YouTube cookies</span><span id="sendCookiesStatus"></span>
</div>
<div>
<input type="checkbox" id="autostart" name="autostart">
<span>Autostart Downloads</span>
</div>
<input type="checkbox" id="sendCookies" name="sendCookies">
<span>Sync YouTube cookies</span><span id="sendCookiesStatus"></span>
</div>
<hr>
<div class="icons">

@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "TubeArchivist Companion",
"description": "Interact with your selfhosted TA server.",
"version": "0.3.1",
"version": "0.1.3",
"icons": {
"48": "/images/icon.png",
"128": "/images/icon128.png"

@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "TubeArchivist Companion",
"description": "Interact with your selfhosted TA server.",
"version": "0.3.1",
"version": "0.1.3",
"icons": {
"128": "/images/icon128.png"
},

@ -38,11 +38,6 @@ function clearError() {
errorOut.style.display = 'none';
}
function clearTempLocalStorage() {
browserType.storage.local.remove('popupApiKey');
browserType.storage.local.remove('popupFullUrl');
}
// store access details
document.getElementById('save-login').addEventListener('click', function () {
let url = document.getElementById('full-url').value;
@ -78,25 +73,6 @@ document.getElementById('sendCookies').addEventListener('click', function () {
sendCookie();
});
// autostart
document.getElementById('autostart').addEventListener('click', function () {
toggleAutostart();
});
let fullUrlInput = document.getElementById('full-url');
fullUrlInput.addEventListener('change', () => {
browserType.storage.local.set({
popupFullUrl: fullUrlInput.value,
});
});
let apiKeyInput = document.getElementById('api-key');
apiKeyInput.addEventListener('change', () => {
browserType.storage.local.set({
popupApiKey: apiKeyInput.value,
});
});
function sendCookie() {
console.log('popup send cookie');
clearError();
@ -128,22 +104,9 @@ function sendCookie() {
sending.then(handleResponse, handleError);
}
function toggleAutostart() {
let checked = document.getElementById('autostart').checked;
let toStore = {
autostart: {
checked: checked,
},
};
browserType.storage.local.set(toStore, function () {
console.log('stored option: ' + JSON.stringify(toStore));
});
}
// send ping message to TA backend
function pingBackend() {
clearError();
clearTempLocalStorage();
function handleResponse() {
console.log('connection validated');
setStatusIcon(true);
@ -204,12 +167,6 @@ document.addEventListener('DOMContentLoaded', async () => {
function onGot(item) {
if (!item.access) {
console.log('no access details found');
if (item.popupFullUrl != null && fullUrlInput.value === '') {
fullUrlInput.value = item.popupFullUrl;
}
if (item.popupApiKey != null && apiKeyInput.value === '') {
apiKeyInput.value = item.popupApiKey;
}
setStatusIcon(false);
return;
}
@ -233,25 +190,11 @@ document.addEventListener('DOMContentLoaded', async () => {
setCookieState();
}
function setAutostartOption(result) {
console.log(result);
if (!result.autostart || result.autostart.checked === false) {
console.log('autostart not set');
return;
}
console.log('set options: ' + JSON.stringify(result));
document.getElementById('autostart').checked = true;
}
browserType.storage.local.get(['access', 'popupFullUrl', 'popupApiKey'], function (result) {
browserType.storage.local.get('access', function (result) {
onGot(result);
});
browserType.storage.local.get('sendCookies', function (result) {
setCookiesOptions(result);
});
browserType.storage.local.get('autostart', function (result) {
setAutostartOption(result);
});
});

@ -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>
</svg>`;
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': '8px',
borderRadius: '18px',
margin: '5px',
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(titleContainer);
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');
let 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 = downloadIcon;
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 = 'Unsubscribe';
buttonSpan.innerHTML = 'Subscribe';
}, 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);
}
});

@ -63,7 +63,7 @@ hr {
color: #00202f;
}
.options {
display: block;
display: flex;
padding-bottom: 10px;
}
.options span {

Loading…
Cancel
Save