Compare commits
58 Commits
Author | SHA1 | Date |
---|---|---|
Simon | 1d27545409 | |
Simon | 7b40dc44c2 | |
Simon | 9ba90e4e45 | |
Simon | 0d58ddaaa2 | |
Vladimir Pouzanov | c82e493628 | |
Simon | ca15cc9c0b | |
Simon | 190f545ef2 | |
Ritiek Malhotra | 761030ca55 | |
Simon | 82a64ff4ba | |
Simon | 6a990ba11b | |
Simon | 988b2d59f4 | |
Simon | 999e86d637 | |
Kevin Gibbons | 92bef81e37 | |
Simon | 5987707b53 | |
Simon | f8d69f5883 | |
Simon | 4f54e1f863 | |
Kevin Gibbons | 75848ad4eb | |
Kevin Gibbons | ee6db2595f | |
Kevin Gibbons | 72c94fbe99 | |
Kevin Gibbons | c570aff66d | |
Simon | aaa04a43b5 | |
Simon | 976fefbf89 | |
Simon | 35186c09ca | |
Gautam krishna R | 160580a2a6 | |
Simon | 5406007315 | |
Simon | 79a002956b | |
Simon | f5f919dfef | |
Simon | dfaf7612ce | |
Simon | bcf8d205d4 | |
Simon | f0abc1af26 | |
crocs | 93ee803229 | |
Simon | c3303a4d13 | |
Merlin | da4345a985 | |
Simon | dc8fecf792 | |
Simon | fd87615cdc | |
Simon | ad2a6f3693 | |
Simon | c79c0cc408 | |
Simon | f3064f32b1 | |
Simon | 1306dbd6fa | |
Simon | 87ef597116 | |
Simon | ef89daf1a1 | |
Simon | 7c47c980f3 | |
Simon | adde4c51c0 | |
Simon | 4112501900 | |
Simon | 114548d362 | |
Simon | 1fed4c32e2 | |
Simon | f9feee70d1 | |
Simon | bf1c47843f | |
Simon | cd5e9a8c0a | |
Simon | c3079d81ff | |
Simon | 004067a1f7 | |
Simon | 10ecdaee23 | |
Simon | f0ec9e23a7 | |
simon | 8cad2bcc22 | |
simon | fe309560da | |
simon | 0d611538a3 | |
simon | 7ce3835ef3 | |
simon | 62fa12d218 |
|
@ -6,6 +6,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
|
es6: true,
|
||||||
},
|
},
|
||||||
globals: {
|
globals: {
|
||||||
browser: 'readonly',
|
browser: 'readonly',
|
||||||
|
@ -17,5 +18,7 @@ module.exports = {
|
||||||
eqeqeq: ['error', 'always', { null: 'ignore' }],
|
eqeqeq: ['error', 'always', { null: 'ignore' }],
|
||||||
curly: ['error', 'multi-line'],
|
curly: ['error', 'multi-line'],
|
||||||
'no-var': 'error',
|
'no-var': 'error',
|
||||||
|
'no-func-assign': 'off',
|
||||||
|
'no-inner-declarations': 'off',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
30
README.md
30
README.md
|
@ -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.
|
- 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 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.
|
- 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 to reveal a download button for that video.
|
- Throughout most places, hover over the video title to reveal a download button for that video.
|
||||||
- Sync your cookies for yt-dlp.
|
- Sync your cookies for yt-dlp.
|
||||||
|
|
||||||
## Screenshots
|
## 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.
|
Popup to enter your connection details.
|
||||||
<br><br>
|
<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.
|
Button injected on video page to download the video or subscribe to the channel.
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
![search page](assets/screenshot-search.png?raw=true "Tube Archivist Companion Search Page")
|
![search page](assets/tac-screenshot-search.jpg?raw=true "Tube Archivist Companion Search Page")
|
||||||
Download button injected showing when hovering over top left corned of thumbnail
|
Download button injected showing when hovering over the video title.
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
![channel page](assets/screenshot-channel.png?raw=true "Tube Archivist Companion Channel Page")
|
![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 topleft corner of thumbnail.
|
Channel button injected to subscribe or download whole channel, video download button showing when hovering over the video title.
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
@ -51,9 +51,13 @@ A green checkmark will appear next to the *Save* button if your connection is wo
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
- **Sync YouTube cookies**: Send your cookies to TubeArchivist to use for yt-dlp requests.
|
- **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
|
## 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
|
- Firefox
|
||||||
- Open `about:debugging#/runtime/this-firefox`
|
- Open `about:debugging#/runtime/this-firefox`
|
||||||
- Click on *Load Temporary Add-on*
|
- Click on *Load Temporary Add-on*
|
||||||
|
@ -72,12 +76,16 @@ Use the correct manifest file for your browser. Either rename the browser specif
|
||||||
|
|
||||||
## Roadmap
|
## 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:
|
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
|
- [ ] Implement download/subscribe button for playlists
|
||||||
- [ ] Implement download button for videos on playlist
|
- [ ] Add download buttons to the `/shorts/` pages
|
||||||
- [ ] Error handling for connection errors
|
- [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
|
||||||
- [X] Dynamically inject buttons with mutation observer
|
- [X] Dynamically inject buttons with mutation observer
|
||||||
|
|
||||||
## Making changes to the JavaScript
|
## 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.
|
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 |
Binary file not shown.
After Width: | Height: | Size: 124 KiB |
|
@ -101,8 +101,13 @@ async function verifyConnection() {
|
||||||
|
|
||||||
// send youtube link from injected buttons
|
// send youtube link from injected buttons
|
||||||
async function download(url) {
|
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(
|
return await sendData(
|
||||||
'api/download/',
|
apiURL,
|
||||||
{
|
{
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
|
@ -115,14 +120,14 @@ async function download(url) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function subscribe(url) {
|
async function subscribe(url, subscribed) {
|
||||||
return await sendData(
|
return await sendData(
|
||||||
'api/channel/',
|
'api/channel/',
|
||||||
{
|
{
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
channel_id: url,
|
channel_id: url,
|
||||||
channel_subscribed: true,
|
channel_subscribed: subscribed,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -130,6 +135,55 @@ async function subscribe(url) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
async function cookieStr(cookieLines) {
|
||||||
const path = 'api/cookie/';
|
const path = 'api/cookie/';
|
||||||
let payload = {
|
let payload = {
|
||||||
|
@ -154,6 +208,7 @@ function buildCookieLine(cookie) {
|
||||||
|
|
||||||
async function sendCookies() {
|
async function sendCookies() {
|
||||||
console.log('function sendCookies');
|
console.log('function sendCookies');
|
||||||
|
const acceptableDomains = ['.youtube.com', 'youtube.com', 'www.youtube.com'];
|
||||||
|
|
||||||
let cookieStores = await browserType.cookies.getAllCookieStores();
|
let cookieStores = await browserType.cookies.getAllCookieStores();
|
||||||
let cookieLines = [
|
let cookieLines = [
|
||||||
|
@ -169,7 +224,9 @@ async function sendCookies() {
|
||||||
});
|
});
|
||||||
for (let j = 0; j < allCookiesStore.length; j++) {
|
for (let j = 0; j < allCookiesStore.length; j++) {
|
||||||
const cookie = allCookiesStore[j];
|
const cookie = allCookiesStore[j];
|
||||||
cookieLines.push(buildCookieLine(cookie));
|
if (acceptableDomains.includes(cookie.domain)) {
|
||||||
|
cookieLines.push(buildCookieLine(cookie));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,6 +244,9 @@ type Message =
|
||||||
| { type: 'sendCookie' }
|
| { type: 'sendCookie' }
|
||||||
| { type: 'download', url: string }
|
| { type: 'download', url: string }
|
||||||
| { type: 'subscribe', url: string }
|
| { type: 'subscribe', url: string }
|
||||||
|
| { type: 'unsubscribe', url: string }
|
||||||
|
| { type: 'videoExists', id: string }
|
||||||
|
| { type: 'getChannel', url: string }
|
||||||
*/
|
*/
|
||||||
function handleMessage(request, sender, sendResponse) {
|
function handleMessage(request, sender, sendResponse) {
|
||||||
console.log('message background.js listener got message', request);
|
console.log('message background.js listener got message', request);
|
||||||
|
@ -209,7 +269,17 @@ function handleMessage(request, sender, sendResponse) {
|
||||||
return await download(request.url);
|
return await download(request.url);
|
||||||
}
|
}
|
||||||
case 'subscribe': {
|
case 'subscribe': {
|
||||||
return await subscribe(request.url);
|
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);
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
let err = new Error(`unknown message type ${JSON.stringify(request.type)}`);
|
let err = new Error(`unknown message type ${JSON.stringify(request.type)}`);
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<a href="#" id="ta-url" target="_blank">
|
<a href="#" id="ta-url" target="_blank">
|
||||||
<img src="/images/logo.png" alt="ta-logo">
|
<img src="/images/logo.png" alt="ta-logo">
|
||||||
</a>
|
</a>
|
||||||
<span>v0.1.3</span>
|
<span>v0.3.1</span>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<form class="login-form">
|
<form class="login-form">
|
||||||
|
@ -30,8 +30,14 @@
|
||||||
<hr>
|
<hr>
|
||||||
<p>Options:</p>
|
<p>Options:</p>
|
||||||
<div class="options">
|
<div class="options">
|
||||||
<input type="checkbox" id="sendCookies" name="sendCookies">
|
<div>
|
||||||
<span>Sync YouTube cookies</span><span id="sendCookiesStatus"></span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="icons">
|
<div class="icons">
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "TubeArchivist Companion",
|
"name": "TubeArchivist Companion",
|
||||||
"description": "Interact with your selfhosted TA server.",
|
"description": "Interact with your selfhosted TA server.",
|
||||||
"version": "0.1.3",
|
"version": "0.3.1",
|
||||||
"icons": {
|
"icons": {
|
||||||
"48": "/images/icon.png",
|
"48": "/images/icon.png",
|
||||||
"128": "/images/icon128.png"
|
"128": "/images/icon128.png"
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "TubeArchivist Companion",
|
"name": "TubeArchivist Companion",
|
||||||
"description": "Interact with your selfhosted TA server.",
|
"description": "Interact with your selfhosted TA server.",
|
||||||
"version": "0.1.3",
|
"version": "0.3.1",
|
||||||
"icons": {
|
"icons": {
|
||||||
"128": "/images/icon128.png"
|
"128": "/images/icon128.png"
|
||||||
},
|
},
|
||||||
|
|
|
@ -38,6 +38,11 @@ function clearError() {
|
||||||
errorOut.style.display = 'none';
|
errorOut.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearTempLocalStorage() {
|
||||||
|
browserType.storage.local.remove('popupApiKey');
|
||||||
|
browserType.storage.local.remove('popupFullUrl');
|
||||||
|
}
|
||||||
|
|
||||||
// store access details
|
// store access details
|
||||||
document.getElementById('save-login').addEventListener('click', function () {
|
document.getElementById('save-login').addEventListener('click', function () {
|
||||||
let url = document.getElementById('full-url').value;
|
let url = document.getElementById('full-url').value;
|
||||||
|
@ -73,6 +78,25 @@ document.getElementById('sendCookies').addEventListener('click', function () {
|
||||||
sendCookie();
|
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() {
|
function sendCookie() {
|
||||||
console.log('popup send cookie');
|
console.log('popup send cookie');
|
||||||
clearError();
|
clearError();
|
||||||
|
@ -104,9 +128,22 @@ function sendCookie() {
|
||||||
sending.then(handleResponse, handleError);
|
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
|
// send ping message to TA backend
|
||||||
function pingBackend() {
|
function pingBackend() {
|
||||||
clearError();
|
clearError();
|
||||||
|
clearTempLocalStorage();
|
||||||
function handleResponse() {
|
function handleResponse() {
|
||||||
console.log('connection validated');
|
console.log('connection validated');
|
||||||
setStatusIcon(true);
|
setStatusIcon(true);
|
||||||
|
@ -167,6 +204,12 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
function onGot(item) {
|
function onGot(item) {
|
||||||
if (!item.access) {
|
if (!item.access) {
|
||||||
console.log('no access details found');
|
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);
|
setStatusIcon(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -190,11 +233,25 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
setCookieState();
|
setCookieState();
|
||||||
}
|
}
|
||||||
|
|
||||||
browserType.storage.local.get('access', function (result) {
|
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) {
|
||||||
onGot(result);
|
onGot(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
browserType.storage.local.get('sendCookies', function (result) {
|
browserType.storage.local.get('sendCookies', function (result) {
|
||||||
setCookiesOptions(result);
|
setCookiesOptions(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
browserType.storage.local.get('autostart', function (result) {
|
||||||
|
setAutostartOption(result);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,32 +4,6 @@ content script running on youtube.com
|
||||||
|
|
||||||
'use strict';
|
'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"
|
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">
|
viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="preserve">
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
|
@ -111,193 +85,41 @@ viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="pres
|
||||||
</g>
|
</g>
|
||||||
</svg>`;
|
</svg>`;
|
||||||
|
|
||||||
function buildButtonDiv() {
|
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 buttonDiv = document.createElement('div');
|
|
||||||
buttonDiv.setAttribute('id', 'ta-channel-button');
|
|
||||||
|
|
||||||
Object.assign(buttonDiv.style, {
|
let browserType = getBrowser();
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: '#00202f',
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: '14px',
|
|
||||||
padding: '5px',
|
|
||||||
margin: '5px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
});
|
|
||||||
return buttonDiv;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildSubLink(channelContainer) {
|
// boilerplate to dedect browser type api
|
||||||
let subLink = document.createElement('span');
|
function getBrowser() {
|
||||||
subLink.innerText = 'Subscribe';
|
if (typeof chrome !== 'undefined') {
|
||||||
subLink.addEventListener('click', e => {
|
if (typeof browser !== 'undefined') {
|
||||||
e.preventDefault();
|
console.log('detected firefox');
|
||||||
let currentLocation = window.location.href;
|
return browser;
|
||||||
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 {
|
} else {
|
||||||
subText = channelContainer.querySelector('#text').textContent;
|
console.log('detected chrome');
|
||||||
|
return chrome;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
e.target.title = 'TA Subscribe: ' + subText;
|
console.log('failed to dedect browser');
|
||||||
});
|
throw 'browser detection error';
|
||||||
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() {
|
function getChannelContainers() {
|
||||||
let nodes = document.querySelectorAll('#inner-header-container, #owner');
|
const elements = document.querySelectorAll('.yt-flexible-actions-view-model-wiz, #owner');
|
||||||
return nodes;
|
const channelContainerNodes = [];
|
||||||
|
|
||||||
|
elements.forEach(element => {
|
||||||
|
if (isElementVisible(element)) {
|
||||||
|
channelContainerNodes.push(element);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return channelContainerNodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getThubnailContainers() {
|
function isElementVisible(element) {
|
||||||
let nodes = document.querySelectorAll('#thumbnail');
|
return element.offsetWidth > 0 || element.offsetHeight > 0 || element.getClientRects().length > 0;
|
||||||
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;
|
|
||||||
|
|
||||||
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',
|
|
||||||
backgroundColor: '#00202f',
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: '1.4rem',
|
|
||||||
textDecoration: 'none',
|
|
||||||
borderRadius: '8px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
opacity: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
let dlIcon = document.createElement('span');
|
|
||||||
dlIcon.innerHTML = downloadIcon;
|
|
||||||
Object.assign(dlIcon.style, {
|
|
||||||
filter: 'invert()',
|
|
||||||
width: '18px',
|
|
||||||
padding: '7px 8px',
|
|
||||||
});
|
|
||||||
|
|
||||||
dlButton.appendChild(dlIcon);
|
|
||||||
|
|
||||||
return dlButton;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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() {
|
function ensureTALinks() {
|
||||||
|
@ -311,16 +133,364 @@ function ensureTALinks() {
|
||||||
channelContainer.hasTA = true;
|
channelContainer.hasTA = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let thumbContainerNodes = getThubnailContainers();
|
let titleContainerNodes = getTitleContainers();
|
||||||
|
for (let titleContainer of titleContainerNodes) {
|
||||||
for (let thumbContainer of thumbContainerNodes) {
|
let parent = getNearestH3(titleContainer);
|
||||||
if (thumbContainer.hasTA) continue;
|
if (!parent) continue;
|
||||||
let videoButton = buildVideoButton(thumbContainer);
|
if (parent.hasTA) continue;
|
||||||
|
let videoButton = buildVideoButton(titleContainer);
|
||||||
if (videoButton == null) continue;
|
if (videoButton == null) continue;
|
||||||
thumbContainer.parentElement.appendChild(videoButton);
|
processTitle(parent);
|
||||||
thumbContainer.hasTA = true;
|
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() {
|
||||||
|
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(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 videoId = getVideoId(titleContainer);
|
||||||
|
if (!videoId) return;
|
||||||
|
|
||||||
|
const dlButton = document.createElement('a');
|
||||||
|
dlButton.classList.add('ta-button');
|
||||||
|
dlButton.href = '#';
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
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(e) {
|
||||||
|
buttonError(taButton);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendDownload(button) {
|
||||||
|
let url = button.dataset.id;
|
||||||
|
if (!url) return;
|
||||||
|
sendUrl(url, 'download', button);
|
||||||
|
}
|
||||||
|
|
||||||
function buttonError(button) {
|
function buttonError(button) {
|
||||||
let buttonSpan = button.querySelector('span');
|
let buttonSpan = button.querySelector('span');
|
||||||
|
@ -347,13 +517,10 @@ function buttonSuccess(button) {
|
||||||
if (buttonSpan.innerHTML === 'Subscribe') {
|
if (buttonSpan.innerHTML === 'Subscribe') {
|
||||||
buttonSpan.innerHTML = 'Success';
|
buttonSpan.innerHTML = 'Success';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
buttonSpan.innerHTML = 'Subscribe';
|
buttonSpan.innerHTML = 'Unsubscribe';
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} else {
|
} else {
|
||||||
buttonSpan.innerHTML = checkmarkIcon;
|
buttonSpan.innerHTML = checkmarkIcon;
|
||||||
setTimeout(() => {
|
|
||||||
buttonSpan.innerHTML = downloadIcon;
|
|
||||||
}, 2000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,9 +534,8 @@ function sendUrl(url, action, button) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleError(error) {
|
function handleError(e) {
|
||||||
console.log('error');
|
console.log('error', e);
|
||||||
console.log(JSON.stringify(error));
|
|
||||||
buttonError(button);
|
buttonError(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -381,19 +547,50 @@ function sendUrl(url, action, button) {
|
||||||
sending.then(handleResponse, handleError);
|
sending.then(handleResponse, handleError);
|
||||||
}
|
}
|
||||||
|
|
||||||
let throttleBlock;
|
async function sendMessage(message) {
|
||||||
const throttle = (callback, time) => {
|
let { success, value } = await browserType.runtime.sendMessage(message);
|
||||||
if (throttleBlock) return;
|
if (!success) {
|
||||||
throttleBlock = true;
|
throw value;
|
||||||
setTimeout(() => {
|
}
|
||||||
callback();
|
return value;
|
||||||
throttleBlock = false;
|
}
|
||||||
}, time);
|
|
||||||
};
|
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 observer = new MutationObserver(list => {
|
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)) {
|
if (list.some(i => i.type === 'childList' && i.addedNodes.length > 0)) {
|
||||||
throttle(ensureTALinks, 700);
|
ensureTALinks();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,7 @@ hr {
|
||||||
color: #00202f;
|
color: #00202f;
|
||||||
}
|
}
|
||||||
.options {
|
.options {
|
||||||
display: flex;
|
display: block;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
.options span {
|
.options span {
|
||||||
|
|
Loading…
Reference in New Issue