Compare commits

...

31 Commits

Author SHA1 Message Date
Simon 1d27545409
bump version 2024-05-15 20:47:38 +02:00
Simon 7b40dc44c2
fix checkVideoExists, check siblings in getNearestLink 2024-05-13 21:53:45 +02:00
Simon 9ba90e4e45
bump version 2024-05-11 17:05:25 +02:00
Simon 0d58ddaaa2
limit cookie domains, #22 (#38) 2024-05-11 22:02:46 +07:00
Vladimir Pouzanov c82e493628
Relocate the injected button to the h3 element (#37) 2024-05-11 22:00:29 +07:00
Simon ca15cc9c0b
fix channel page selector 2024-05-01 22:23:41 +02:00
Simon 190f545ef2
fix format linter 2024-05-01 22:23:26 +02:00
Ritiek Malhotra 761030ca55
Show checkmark for already downloaded currently playing videos (#35)
Looks like this was regressed from v0.2.1 to v0.2.2 in this commit sha:
f8d69f5883

Already downloaded video currently playing in YouTube were still
showing the download icon instead of checkmark icon in TA.

This commit should fix this.
2024-05-02 02:20:34 +07:00
Simon 82a64ff4ba
update roadmap, add shorts pages button 2023-11-10 11:41:30 +07:00
Simon 6a990ba11b
bump version 2023-11-10 11:23:08 +07:00
Simon 988b2d59f4
better testing instructions 2023-11-09 20:26:08 +07:00
Simon 999e86d637
add clearTempLocalStorage 2023-11-09 20:07:32 +07:00
Kevin Gibbons 92bef81e37
persist inputs in popup (#31)
* persist inputs in popup

* consolidate storage requests
2023-11-09 20:06:31 +07:00
Simon 5987707b53
Merge pull request #30 from bakkot/update-subscribe-button
Update subscribe button
video id on hover
2023-11-09 18:23:15 +07:00
Simon f8d69f5883
implement extract video id on hover 2023-11-09 18:21:43 +07:00
Simon 4f54e1f863
flip getChannelHandle logic, remove unvisible title containers 2023-11-08 18:01:50 +07:00
Kevin Gibbons 75848ad4eb fix infinite buttons 2023-11-04 21:39:03 -07:00
Kevin Gibbons ee6db2595f restore .eslintrc.js 2023-11-03 09:05:43 -07:00
Kevin Gibbons 72c94fbe99 better logic for updating the subscribe button on page navigation 2023-11-03 09:05:42 -07:00
Kevin Gibbons c570aff66d Revert "delay buildChannelButton to account for UI refresh on YT"
This reverts commit aaa04a43b5.
2023-11-03 09:00:39 -07:00
Simon aaa04a43b5
delay buildChannelButton to account for UI refresh on YT 2023-11-03 12:05:17 +07:00
Simon 976fefbf89
better fail to get TA info log message 2023-11-02 20:30:17 +07:00
Simon 35186c09ca
skip empty href container link building 2023-11-02 20:25:15 +07:00
Gautam krishna R 160580a2a6
updated the subscribe button styling to match the new youtube ui (#29)
* updated the  subscribe button styling to match new youtube ui

* added code watch page subscribe button
2023-11-02 20:11:25 +07:00
Simon 5406007315
add dl button to shorts results 2023-10-31 16:59:38 +07:00
Simon 79a002956b
reduce single button size 2023-10-31 16:48:58 +07:00
Simon f5f919dfef
fix video id extraction for url with additional query params 2023-10-19 10:25:10 +07:00
Simon dfaf7612ce
fix button location wording 2023-10-07 16:53:08 +07:00
Simon bcf8d205d4
fix jpg link 2023-10-07 16:51:39 +07:00
Simon f0abc1af26
resize screenshots 2023-10-07 16:50:19 +07:00
crocs 93ee803229
Updated Screenshots (#26)
* Delete assets/screenshot.png

* Delete assets/screenshot-search.png

* Delete assets/screenshot-channel.png

* Add files via upload

* Update README.md

* Delete assets/screenshot-channel.png

* Delete assets/screenshot.png

* Delete assets/screenshot-search.png

* Add files via upload

* Update README.md
2023-10-07 16:33:49 +07:00
14 changed files with 194 additions and 77 deletions

View File

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

View File

@ -15,7 +15,7 @@ This is a browser extension to bridge YouTube with [Tube Archivist](https://gith
- Sync your cookies for yt-dlp.
## Screenshots
![popup screenshot](assets/screenshot.png?raw=true "Tube Archivist Companion Popup")
![popup screenshot](assets/tac-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/screenshot-search.png?raw=true "Tube Archivist Companion Search Page")
Download button injected showing when hovering over top left corned of thumbnail
![search page](assets/tac-screenshot-search.jpg?raw=true "Tube Archivist Companion Search Page")
Download button injected showing when hovering over the video title.
<br><br>
![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.
![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.
<br>
## Install
@ -54,7 +54,10 @@ A green checkmark will appear next to the *Save* button if your connection is wo
- **Autostart**: Autostart and prioritize videos send from this extension.
## Test this extension
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`.
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`.
- Firefox
- Open `about:debugging#/runtime/this-firefox`
- Click on *Load Temporary Add-on*
@ -74,6 +77,7 @@ Use the correct manifest file for your browser. Either rename the browser specif
## 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:
- [ ] 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
@ -82,3 +86,6 @@ Join us on [Discord](https://www.tubearchivist.com/discord) and help us improve
## 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.

Before

Width:  |  Height:  |  Size: 490 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

BIN
assets/tac-screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View File

@ -208,6 +208,7 @@ 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 = [
@ -223,7 +224,9 @@ async function sendCookies() {
});
for (let j = 0; j < allCookiesStore.length; j++) {
const cookie = allCookiesStore[j];
cookieLines.push(buildCookieLine(cookie));
if (acceptableDomains.includes(cookie.domain)) {
cookieLines.push(buildCookieLine(cookie));
}
}
}

View File

@ -14,7 +14,7 @@
<a href="#" id="ta-url" target="_blank">
<img src="/images/logo.png" alt="ta-logo">
</a>
<span>v0.2.1</span>
<span>v0.3.1</span>
</div>
<hr>
<form class="login-form">

View File

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

View File

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

View File

@ -38,6 +38,11 @@ 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,6 +83,20 @@ 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();
@ -124,6 +143,7 @@ function toggleAutostart() {
// send ping message to TA backend
function pingBackend() {
clearError();
clearTempLocalStorage();
function handleResponse() {
console.log('connection validated');
setStatusIcon(true);
@ -184,6 +204,12 @@ 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;
}
@ -217,7 +243,7 @@ document.addEventListener('DOMContentLoaded', async () => {
document.getElementById('autostart').checked = true;
}
browserType.storage.local.get('access', function (result) {
browserType.storage.local.get(['access', 'popupFullUrl', 'popupApiKey'], function (result) {
onGot(result);
});

View File

@ -106,7 +106,7 @@ function getBrowser() {
}
function getChannelContainers() {
const elements = document.querySelectorAll('#inner-header-container, #owner');
const elements = document.querySelectorAll('.yt-flexible-actions-view-model-wiz, #owner');
const channelContainerNodes = [];
elements.forEach(element => {
@ -135,69 +135,87 @@ function ensureTALinks() {
let titleContainerNodes = getTitleContainers();
for (let titleContainer of titleContainerNodes) {
if (titleContainer.hasTA) continue;
let parent = getNearestH3(titleContainer);
if (!parent) continue;
if (parent.hasTA) continue;
let videoButton = buildVideoButton(titleContainer);
if (videoButton == null) continue;
processTitle(titleContainer);
titleContainer.appendChild(videoButton);
titleContainer.hasTA = true;
processTitle(parent);
parent.appendChild(videoButton);
parent.hasTA = true;
}
}
ensureTALinks = throttled(ensureTALinks, 700);
// 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;
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) {
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];
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() {
let buttonDiv = document.createElement('div');
buttonDiv.classList.add('ta-channel-button');
Object.assign(buttonDiv.style, {
display: 'flex',
alignItems: 'center',
@ -205,8 +223,8 @@ function buildChannelButtonDiv() {
color: '#fff',
fontSize: '14px',
padding: '5px',
margin: '5px',
borderRadius: '8px',
'margin-left': '8px',
borderRadius: '18px',
});
return buttonDiv;
}
@ -249,10 +267,10 @@ function checkChannelSubscribed(channelSubButton) {
console.log('Unknown state');
}
}
function handleError() {
function handleError(e) {
buttonError(channelSubButton);
channelSubButton.innerText = 'Error';
console.log('error');
console.error('error', e);
}
let channelHandle = channelSubButton.dataset.id;
@ -274,7 +292,8 @@ function buildChannelDownloadButton() {
let urlObj = new URL(currentLocation);
if (urlObj.pathname.startsWith('/watch')) {
let videoId = urlObj.search.split('=')[1];
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}`;
@ -301,24 +320,40 @@ function buildChannelDownloadButton() {
}
function getTitleContainers() {
let nodes = document.querySelectorAll('#video-title');
return nodes;
let elements = document.querySelectorAll('#video-title');
let videoNodes = [];
elements.forEach(element => {
if (isElementVisible(element)) {
videoNodes.push(element);
}
});
return elements;
}
function getVideoId(titleContainer) {
if (!titleContainer) return undefined;
let href = getNearestLink(titleContainer);
if (!href) return;
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;
}
function buildVideoButton(titleContainer) {
let href = getNearestLink(titleContainer);
let videoId = getVideoId(titleContainer);
if (!videoId) return;
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',
@ -337,8 +372,8 @@ function buildVideoButton(titleContainer) {
dlIcon.innerHTML = defaultIcon;
Object.assign(dlIcon.style, {
filter: 'invert()',
width: '18px',
height: '18px',
width: '15px',
height: '15px',
padding: '7px 8px',
});
@ -354,6 +389,24 @@ function buildVideoButton(titleContainer) {
}
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');
@ -363,8 +416,18 @@ function getNearestLink(element) {
return null;
}
function getNearestH3(element) {
for (let i = 0; i < 5 && element && element !== document; i++) {
if (element.tagName === 'H3') {
return element;
}
element = element.parentNode;
}
return null;
}
function processTitle(titleContainer) {
if (titleContainer.hasListener) return
if (titleContainer.hasListener) return;
Object.assign(titleContainer.style, {
display: 'flex',
gap: '15px',
@ -401,12 +464,23 @@ function checkVideoExists(taButton) {
}
taButton.isChecked = true;
}
function handleError() {
function handleError(e) {
buttonError(taButton);
console.log('error');
let videoId = taButton.dataset.id;
console.log(`error: failed to get info from TA for video ${videoId}`);
console.error(e);
}
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}]`;
}
}
let message = { type: 'videoExists', videoId };
let sending = sendMessage(message);
sending.then(handleResponse, handleError);
@ -460,9 +534,8 @@ function sendUrl(url, action, button) {
}
}
function handleError(error) {
console.log('error');
console.log(JSON.stringify(error));
function handleError(e) {
console.log('error', e);
buttonError(button);
}
@ -495,15 +568,20 @@ function cleanButtons() {
}
let oldHref = document.location.href;
let throttleBlock;
const throttle = (callback, time) => {
if (throttleBlock) return;
throttleBlock = true;
setTimeout(() => {
callback();
throttleBlock = false;
}, time);
};
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 observer = new MutationObserver(list => {
const currentHref = document.location.href;
@ -512,7 +590,7 @@ let observer = new MutationObserver(list => {
oldHref = currentHref;
}
if (list.some(i => i.type === 'childList' && i.addedNodes.length > 0)) {
throttle(ensureTALinks, 700);
ensureTALinks();
}
});