Compare commits
117 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
f5152a4717 | ||
af61f2a4ab | |||
|
967a52881b | ||
d3f01b372a | |||
1d27545409 | |||
7b40dc44c2 | |||
9ba90e4e45 | |||
|
0d58ddaaa2 | ||
|
c82e493628 | ||
ca15cc9c0b | |||
190f545ef2 | |||
|
761030ca55 | ||
82a64ff4ba | |||
6a990ba11b | |||
988b2d59f4 | |||
999e86d637 | |||
|
92bef81e37 | ||
|
5987707b53 | ||
f8d69f5883 | |||
4f54e1f863 | |||
|
75848ad4eb | ||
|
ee6db2595f | ||
|
72c94fbe99 | ||
|
c570aff66d | ||
aaa04a43b5 | |||
976fefbf89 | |||
35186c09ca | |||
|
160580a2a6 | ||
5406007315 | |||
79a002956b | |||
f5f919dfef | |||
dfaf7612ce | |||
bcf8d205d4 | |||
f0abc1af26 | |||
|
93ee803229 | ||
c3303a4d13 | |||
|
da4345a985 | ||
dc8fecf792 | |||
fd87615cdc | |||
ad2a6f3693 | |||
c79c0cc408 | |||
f3064f32b1 | |||
1306dbd6fa | |||
87ef597116 | |||
ef89daf1a1 | |||
7c47c980f3 | |||
adde4c51c0 | |||
4112501900 | |||
114548d362 | |||
1fed4c32e2 | |||
f9feee70d1 | |||
bf1c47843f | |||
cd5e9a8c0a | |||
c3079d81ff | |||
004067a1f7 | |||
10ecdaee23 | |||
f0ec9e23a7 | |||
8cad2bcc22 | |||
fe309560da | |||
0d611538a3 | |||
7ce3835ef3 | |||
62fa12d218 | |||
144dda3a84 | |||
2cc5cfc17d | |||
|
9528a347e0 | ||
c7069d90cb | |||
9de3d65e28 | |||
44af4dc0b2 | |||
265c8f1e7d | |||
6da4662007 | |||
9d75b13886 | |||
55ef05cc2a | |||
|
8b71d15036 | ||
|
c292d03b3c | ||
|
d239fcf045 | ||
|
80901021d2 | ||
|
ee93db2c00 | ||
|
2e28bf25b6 | ||
|
ade0f1e165 | ||
|
d2ac512296 | ||
|
ec9834f757 | ||
|
5ae0a1baf4 | ||
|
a2e167c9cf | ||
|
b19b09bb84 | ||
|
9fadbd5c15 | ||
|
97deb2141c | ||
|
d01803f605 | ||
|
9c535d27e6 | ||
|
ee7da0e726 | ||
|
64d29b5a4f | ||
|
2dc650c76b | ||
f62f584dda | |||
1d8dda3b81 | |||
|
52b65bf48b | ||
d3e647d0cf | |||
|
18bfa28452 | ||
030fb2d223 | |||
b34d8a822c | |||
69e02e72be | |||
4bafe4d3bb | |||
f9dd81c4f6 | |||
|
d7339d4998 | ||
|
25dd6dbdc5 | ||
|
4ea141c6fe | ||
|
afbcb5757e | ||
|
d5f5557e06 | ||
59709c4c29 | |||
6b6a9b8b02 | |||
122114d099 | |||
e87468900c | |||
30f80ca01a | |||
1aaf4a4411 | |||
4f0452cf5f | |||
a0e4a10f1f | |||
2485715818 | |||
71d915eff1 | |||
da06c2c78b |
24
.eslintrc.js
Normal file
@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
module.exports = {
|
||||
extends: ['eslint:recommended', 'eslint-config-prettier'],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
},
|
||||
globals: {
|
||||
browser: 'readonly',
|
||||
chrome: 'readonly',
|
||||
},
|
||||
rules: {
|
||||
strict: ['error', 'global'],
|
||||
'no-unused-vars': ['error', { vars: 'local' }],
|
||||
eqeqeq: ['error', 'always', { null: 'ignore' }],
|
||||
curly: ['error', 'multi-line'],
|
||||
'no-var': 'error',
|
||||
'no-func-assign': 'off',
|
||||
'no-inner-declarations': 'off',
|
||||
},
|
||||
};
|
16
.github/workflows/lint_js.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
name: lint_js
|
||||
|
||||
on: [pull_request, push]
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: lint_js
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
- run: npm run format -- --check
|
3
.gitignore
vendored
@ -3,3 +3,6 @@ extension/manifest.json
|
||||
|
||||
# release builds
|
||||
release/*
|
||||
|
||||
# JavaScript stuff
|
||||
node_modules
|
||||
|
70
README.md
@ -1,19 +1,35 @@
|
||||
![Tube Archivist Companion](assets/tube-archivist-companion-banner.png?raw=true "Tube Archivist Companion Banner")
|
||||
|
||||
<h1 align="center">Tube Archivist Companion for your Browser</h1>
|
||||
<h1 align="center">Browser Extension for Tube Archivist</h1>
|
||||
<div align="center">
|
||||
<a href="https://www.tilefy.me" target="_blank"><img src="https://tiles.tilefy.me/t/tubearchivist-firefox.png" alt="tubearchivist-firefox" title="TA Companion Firefox users" height="50" width="190"/></a>
|
||||
<a href="https://www.tilefy.me" target="_blank"><img src="https://tiles.tilefy.me/t/tubearchivist-chrome.png" alt="tubearchivist-chrome" title="TA Companion Chrome users" height="50" width="190"/></a>
|
||||
<a href="https://addons.mozilla.org/addon/tubearchivist-companion/" target="_blank"><img src="https://tiles.tilefy.me/t/tubearchivist-firefox.png" alt="tubearchivist-firefox" title="TA Companion Firefox users" height="50" width="190"/></a>
|
||||
<a href="https://chrome.google.com/webstore/detail/tubearchivist-companion/jjnkmicfnfojkkgobdfeieblocadmcie" target="_blank"><img src="https://tiles.tilefy.me/t/tubearchivist-chrome.png" alt="tubearchivist-chrome" title="TA Companion Chrome users" height="50" width="190"/></a>
|
||||
</div>
|
||||
|
||||
## Core Functionality
|
||||
A browser extension to bridge YouTube with [Tube Archivist](https://github.com/tubearchivist/tubearchivist). This extension allows you to do the following:
|
||||
- Add your Tube Archivist connection details in the addon popup
|
||||
- Add a download button to the popup for YouTube links
|
||||
- Add a subscribe button to subscribe to channels and playlists
|
||||
This is a browser extension to bridge YouTube with [Tube Archivist](https://github.com/tubearchivist/tubearchivist), your self hosted YouTube media server.
|
||||
- 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.
|
||||
- Sync your cookies for yt-dlp.
|
||||
|
||||
![popup screenshot](assets/screenshot.png?raw=true "Tube Archivist Companion Popup")
|
||||
## Screenshots
|
||||
![popup screenshot](assets/tac-screenshot.png?raw=true "Tube Archivist Companion Popup")
|
||||
Popup to enter your connection details.
|
||||
<br><br>
|
||||
|
||||
![video page](assets/screenshot-video.png?raw=true "Tube Archivist Companion Video Page")
|
||||
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.
|
||||
<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.
|
||||
<br>
|
||||
|
||||
## Install
|
||||
- Firefox: The addon is available on the [Extension store](https://addons.mozilla.org/addon/tubearchivist-companion/).
|
||||
@ -23,35 +39,53 @@ A browser extension to bridge YouTube with [Tube Archivist](https://github.com/t
|
||||
After a new release here on GitHub, you'll get updates automatically in your browser. Due to the verification process, for Firefox this usually takes 1-2 hours, for Chrome 2-3 days.
|
||||
|
||||
## Permissions
|
||||
- **Access your data for www.youtube.com**: Needed for the addon to know your current page on YouTube to send the link to Tube Archivist.
|
||||
- **Storage**: Needed to store your connection details, needed to store your last visited YouTube link within the browser.
|
||||
- **Access your data for www.youtube.com**: Needed to inject download and subscribe buttons directly into the page.
|
||||
- **Storage**: Needed to store your connection details.
|
||||
- **Cookie**: Needed to read your cookies for youtube.com to access restricted videos.
|
||||
|
||||
## Setup
|
||||
- **URL**: This is where your Tube Archivist instance is located. Can be a host name or a IP address, use a full URL with protocol, e.g. *http://*.
|
||||
- **Port**: Network port of TA.
|
||||
- **API key**: You can find your API key on the settings page of your Tube Archivist instance.
|
||||
- **URL**: This is where your Tube Archivist instance is located. Can be a host name or an IP address. Add the port if needed at the end, e.g. `:8000`.
|
||||
- **API key**: You can find your API key on the settings page (Settings -> Application -> Integrations section -> API token) of your Tube Archivist instance.
|
||||
|
||||
A green checkmark will appear next to the *Save* button if your connection is working.
|
||||
|
||||
## 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
|
||||
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*
|
||||
- Select the *manifest.json* file to load the addon.
|
||||
- You can *inspect* background.js by lunching the debug tools from there.
|
||||
- Chrome / Chromium
|
||||
- Open `chrome://extensions/`
|
||||
- Toggle *Developer mode* on top right
|
||||
- Click on *Load unpacked*
|
||||
- Open the folder containing the *manifest.json* file.
|
||||
- Click on *Service Worker* to open the dev tools at background.js.
|
||||
|
||||
## Compatibility
|
||||
- Verify that you are running the latest version of Tube Archivist as the API is under development and will change.
|
||||
- For testing this extension between releases, use the *unstable* builds of Tube Archivist, only for your tesing environment.
|
||||
- Verify that you are running the [latest version](https://github.com/tubearchivist/tubearchivist/releases/latest) of Tube Archivist as the API is under development and will change.
|
||||
- For testing this extension between releases, use the *unstable* builds of Tube Archivist, only for your testing environment.
|
||||
|
||||
## Help needed
|
||||
Join us on [Discord](https://discord.gg/AFwz8nE7BK) and help us improve and extend this project.
|
||||
## 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
|
||||
- [X] 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.
|
||||
|
BIN
assets/icon-128x128.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
assets/screenshot-video.png
Normal file
After Width: | Height: | Size: 121 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 20 KiB |
BIN
assets/tac-screenshot-channel.jpg
Normal file
After Width: | Height: | Size: 169 KiB |
BIN
assets/tac-screenshot-search.jpg
Normal file
After Width: | Height: | Size: 121 KiB |
BIN
assets/tac-screenshot.png
Normal file
After Width: | Height: | Size: 124 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 12 KiB |
@ -8,6 +8,9 @@ if [[ $(basename "$(pwd)") != 'tubearchivist_browserextension' ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "latest tags:"
|
||||
git tag | tail -n 5 | sort -r
|
||||
|
||||
printf "\ncreate new version:\n"
|
||||
read -r VERSION
|
||||
|
||||
|
@ -2,238 +2,302 @@
|
||||
extension background script listening for events
|
||||
*/
|
||||
|
||||
console.log("running background.js");
|
||||
'use strict';
|
||||
|
||||
console.log('running background.js');
|
||||
|
||||
let browserType = getBrowser();
|
||||
|
||||
|
||||
// boilerplate to dedect browser type api
|
||||
function getBrowser() {
|
||||
if (typeof chrome !== "undefined") {
|
||||
if (typeof browser !== "undefined") {
|
||||
if (typeof chrome !== 'undefined') {
|
||||
if (typeof browser !== 'undefined') {
|
||||
return browser;
|
||||
} else {
|
||||
return chrome;
|
||||
}
|
||||
} else {
|
||||
console.log("failed to dedect browser");
|
||||
throw "browser detection error"
|
||||
};
|
||||
console.log('failed to detect browser');
|
||||
throw 'browser detection error';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// send get request to API backend
|
||||
async function sendGet(path) {
|
||||
|
||||
let access = await getAccess();
|
||||
const url = `${access.url}:${access.port}/${path}`;
|
||||
console.log("GET: " + url);
|
||||
console.log('GET: ' + url);
|
||||
|
||||
const rawResponse = await fetch(url, {
|
||||
method: "GET",
|
||||
method: 'GET',
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Token " + access.apiKey,
|
||||
"mode": "no-cors"
|
||||
}
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Token ' + access.apiKey,
|
||||
mode: 'no-cors',
|
||||
},
|
||||
});
|
||||
|
||||
const content = await rawResponse.json();
|
||||
return content;
|
||||
}
|
||||
|
||||
|
||||
// send post/put request to API backend
|
||||
async function sendData(path, payload, method) {
|
||||
|
||||
let access = await getAccess();
|
||||
const url = `${access.url}:${access.port}/${path}`;
|
||||
console.log(`${method}: ${url}`);
|
||||
console.log(`${method}: ${JSON.stringify(payload)}`);
|
||||
|
||||
try {
|
||||
const rawResponse = await fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Token " + access.apiKey,
|
||||
"mode": "no-cors"
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Token ' + access.apiKey,
|
||||
mode: 'no-cors',
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const content = await rawResponse.json();
|
||||
return content;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// read access details from storage.local
|
||||
async function getAccess() {
|
||||
let storage = await browserType.storage.local.get('access');
|
||||
|
||||
var storage = await browserType.storage.local.get("access");
|
||||
|
||||
return storage.access
|
||||
|
||||
return storage.access;
|
||||
}
|
||||
|
||||
|
||||
// check if cookie is valid
|
||||
async function getCookieState() {
|
||||
const path = 'api/cookie/';
|
||||
let response = await sendGet(path);
|
||||
console.log('cookie state: ' + JSON.stringify(response));
|
||||
|
||||
const path = "api/cookie/";
|
||||
let response = await sendGet(path)
|
||||
console.log("cookie state: " + JSON.stringify(response));
|
||||
|
||||
return response
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
// send ping to server, return response
|
||||
// send ping to server
|
||||
async function verifyConnection() {
|
||||
const path = 'api/ping/';
|
||||
let message = await sendGet(path);
|
||||
console.log('verify connection: ' + JSON.stringify(message));
|
||||
|
||||
const path = "api/ping/";
|
||||
let response = await sendGet(path)
|
||||
console.log("verify connection: " + JSON.stringify(response));
|
||||
|
||||
return response
|
||||
if (message?.response === 'pong') {
|
||||
return true;
|
||||
} else if (message?.detail) {
|
||||
throw new Error(message.detail);
|
||||
} else {
|
||||
throw new Error(`got unknown message ${JSON.stringify(message)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// store last youtube link
|
||||
function setYoutubeLink(data) {
|
||||
browserType.storage.local.set(data, function() {
|
||||
console.log("Stored history: " + JSON.stringify(data));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// send download task to server, return response
|
||||
async function downloadLink(toDownload) {
|
||||
|
||||
const path = "api/download/";
|
||||
let payload = {
|
||||
"data": [
|
||||
// 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,
|
||||
{
|
||||
"youtube_id": toDownload,
|
||||
"status": "pending",
|
||||
}
|
||||
]
|
||||
}
|
||||
let response = await sendData(path, payload, "POST")
|
||||
|
||||
return response
|
||||
|
||||
}
|
||||
|
||||
async function subscribeLink(toSubscribe) {
|
||||
|
||||
const path = "api/channel/";
|
||||
let payload = {
|
||||
"data": [
|
||||
data: [
|
||||
{
|
||||
"channel_id": toSubscribe,
|
||||
"channel_subscribed": true,
|
||||
}
|
||||
]
|
||||
}
|
||||
let response = await sendData(path, payload, "POST");
|
||||
|
||||
return response
|
||||
|
||||
youtube_id: url,
|
||||
status: 'pending',
|
||||
},
|
||||
],
|
||||
},
|
||||
'POST'
|
||||
);
|
||||
}
|
||||
|
||||
async function subscribe(url, subscribed) {
|
||||
return await sendData(
|
||||
'api/channel/',
|
||||
{
|
||||
data: [
|
||||
{
|
||||
channel_id: url,
|
||||
channel_subscribed: subscribed,
|
||||
},
|
||||
],
|
||||
},
|
||||
'POST'
|
||||
);
|
||||
}
|
||||
|
||||
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/";
|
||||
const path = 'api/cookie/';
|
||||
let payload = {
|
||||
"cookie": cookieLines.join("\n")
|
||||
}
|
||||
let response = await sendData(path, payload, "PUT");
|
||||
|
||||
return response
|
||||
cookie: cookieLines.join('\n'),
|
||||
};
|
||||
let response = await sendData(path, payload, 'PUT');
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
function buildCookieLine(cookie) {
|
||||
return [
|
||||
cookie.domain,
|
||||
"TRUE",
|
||||
'TRUE',
|
||||
cookie.path,
|
||||
cookie.httpOnly.toString().toUpperCase(),
|
||||
Math.trunc(cookie.expirationDate) || 0,
|
||||
cookie.name,
|
||||
cookie.value,
|
||||
].join("\t");
|
||||
].join('\t');
|
||||
}
|
||||
|
||||
|
||||
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();
|
||||
var cookieLines = [
|
||||
"# Netscape HTTP Cookie File",
|
||||
"# https://curl.haxx.se/rfc/cookie_spec.html",
|
||||
"# This is a generated file! Do not edit.\n"
|
||||
let cookieLines = [
|
||||
'# Netscape HTTP Cookie File',
|
||||
'# https://curl.haxx.se/rfc/cookie_spec.html',
|
||||
'# This is a generated file! Do not edit.\n',
|
||||
];
|
||||
for (let i = 0; i < cookieStores.length; i++) {
|
||||
const cookieStore = cookieStores[i];
|
||||
var allCookiesStore = await browserType.cookies.getAll({
|
||||
domain: ".youtube.com",
|
||||
storeId: cookieStore["id"]
|
||||
let allCookiesStore = await browserType.cookies.getAll({
|
||||
domain: '.youtube.com',
|
||||
storeId: cookieStore['id'],
|
||||
});
|
||||
for (let j = 0; j < allCookiesStore.length; j++) {
|
||||
const cookie = allCookiesStore[j];
|
||||
if (acceptableDomains.includes(cookie.domain)) {
|
||||
cookieLines.push(buildCookieLine(cookie));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let response = cookieStr(cookieLines);
|
||||
|
||||
return response
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// process and return message if needed
|
||||
/*
|
||||
process and return message if needed
|
||||
the following messages are supported:
|
||||
type Message =
|
||||
| { type: 'verify' }
|
||||
| { type: 'cookieState' }
|
||||
| { 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: " + JSON.stringify(request));
|
||||
console.log('message background.js listener got message', request);
|
||||
|
||||
if (request.verify === true) {
|
||||
let response = verifyConnection();
|
||||
response.then(message => {
|
||||
sendResponse(message);
|
||||
})
|
||||
} else if (request.youtube) {
|
||||
setYoutubeLink(request)
|
||||
} else if (request.download) {
|
||||
let response = downloadLink(request.download.url);
|
||||
response.then(message => {
|
||||
sendResponse(message)
|
||||
})
|
||||
} else if (request.subscribe) {
|
||||
let response = subscribeLink(request.subscribe.url);
|
||||
response.then(message => {
|
||||
sendResponse(message)
|
||||
})
|
||||
} else if (request.cookieState) {
|
||||
let response = getCookieState();
|
||||
response.then(message => {
|
||||
sendResponse(message)
|
||||
})
|
||||
} else if (request.sendCookie) {
|
||||
console.log("backgound: " + JSON.stringify(request));
|
||||
let response = sendCookies();
|
||||
response.then(message => {
|
||||
sendResponse(message)
|
||||
})
|
||||
// this function must return the value `true` in chrome to signal the response will be async;
|
||||
// it cannot return a promise
|
||||
// so in order to use async/await, we need a wrapper
|
||||
(async () => {
|
||||
switch (request.type) {
|
||||
case 'verify': {
|
||||
return await verifyConnection();
|
||||
}
|
||||
|
||||
case 'cookieState': {
|
||||
return await getCookieState();
|
||||
}
|
||||
case 'sendCookie': {
|
||||
return await sendCookies();
|
||||
}
|
||||
case 'download': {
|
||||
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);
|
||||
}
|
||||
default: {
|
||||
let err = new Error(`unknown message type ${JSON.stringify(request.type)}`);
|
||||
console.log(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
})()
|
||||
.then(value => sendResponse({ success: true, value }))
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
let message = e?.message ?? e;
|
||||
if (message === 'Failed to fetch') {
|
||||
// chrome's error message for failed `fetch` is not very user-friendly
|
||||
message = 'Could not connect to server';
|
||||
}
|
||||
sendResponse({ success: false, value: message });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 12 KiB |
@ -14,33 +14,37 @@
|
||||
<a href="#" id="ta-url" target="_blank">
|
||||
<img src="/images/logo.png" alt="ta-logo">
|
||||
</a>
|
||||
<span>v0.0.3</span>
|
||||
<span>v0.3.2</span>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="youtube-page" id="download"></div>
|
||||
<form class="login-form">
|
||||
<label for="url">Tube Archivist IP:</label>
|
||||
<input type="text" id="url" name="url">
|
||||
<label for="port">Tube Archivist Port:</label>
|
||||
<input type="text" id="port" name="port">
|
||||
<label for="full-url">Tube Archivist URL:</label>
|
||||
<input type="text" id="full-url" name="url">
|
||||
<label for="api-key">Tube Archivist API Key:</label>
|
||||
<input type="password" id="api-key" name="api-key">
|
||||
</form>
|
||||
<div class="submit">
|
||||
<button id="save-login">Save</button><span id="status-icon">☐</span>
|
||||
</div>
|
||||
<div id="error-out"></div>
|
||||
<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>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="icons">
|
||||
<div>
|
||||
<a href="https://www.reddit.com/r/TubeArchivist/" target="_blank">
|
||||
<img src="/images/social/reddit.svg" alt="reddit-icon"></a>
|
||||
<a href="https://discord.gg/AFwz8nE7BK" target="_blank">
|
||||
<a href="https://www.tubearchivist.com/discord" target="_blank">
|
||||
<img src="/images/social/discord.svg" alt="discord-icon"></a>
|
||||
<a href="https://github.com/tubearchivist/tubearchivist" target="_blank">
|
||||
<img src="/images/social/github.svg" alt="github-icon">
|
||||
|
@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "TubeArchivist Companion",
|
||||
"description": "Interact with your selfhosted TA server.",
|
||||
"version": "0.0.3",
|
||||
"version": "0.3.2",
|
||||
"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.0.3",
|
||||
"version": "0.3.2",
|
||||
"icons": {
|
||||
"128": "/images/icon128.png"
|
||||
},
|
||||
|
@ -2,280 +2,256 @@
|
||||
Loaded into popup index.html
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
let browserType = getBrowser();
|
||||
|
||||
// boilerplate to dedect browser type api
|
||||
function getBrowser() {
|
||||
if (typeof chrome !== "undefined") {
|
||||
if (typeof browser !== "undefined") {
|
||||
if (typeof chrome !== 'undefined') {
|
||||
if (typeof browser !== 'undefined') {
|
||||
return browser;
|
||||
} else {
|
||||
return chrome;
|
||||
}
|
||||
} else {
|
||||
console.log("failed to dedect browser");
|
||||
throw "browser detection error"
|
||||
};
|
||||
console.log('failed to detect browser');
|
||||
throw 'browser detection error';
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage(message) {
|
||||
let { success, value } = await browserType.runtime.sendMessage(message);
|
||||
if (!success) {
|
||||
throw value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
let errorOut = document.getElementById('error-out');
|
||||
function setError(message) {
|
||||
errorOut.style.display = 'initial';
|
||||
errorOut.innerText = message;
|
||||
}
|
||||
|
||||
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 toStore = {
|
||||
"access": {
|
||||
"url": document.getElementById("url").value,
|
||||
"port": document.getElementById("port").value,
|
||||
"apiKey": document.getElementById("api-key").value
|
||||
document.getElementById('save-login').addEventListener('click', function () {
|
||||
let url = document.getElementById('full-url').value;
|
||||
if (!url.includes('://')) {
|
||||
url = 'http://' + url;
|
||||
}
|
||||
try {
|
||||
clearError();
|
||||
let parsed = new URL(url);
|
||||
let toStore = {
|
||||
access: {
|
||||
url: `${parsed.protocol}//${parsed.hostname}`,
|
||||
port: parsed.port || (parsed.protocol === 'https:' ? '443' : '80'),
|
||||
apiKey: document.getElementById('api-key').value,
|
||||
},
|
||||
};
|
||||
browserType.storage.local.set(toStore, function() {
|
||||
console.log("Stored connection details: " + JSON.stringify(toStore));
|
||||
browserType.storage.local.set(toStore, function () {
|
||||
console.log('Stored connection details: ' + JSON.stringify(toStore));
|
||||
pingBackend();
|
||||
});
|
||||
})
|
||||
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
}
|
||||
});
|
||||
|
||||
// verify connection status
|
||||
document.getElementById("status-icon").addEventListener("click", function() {
|
||||
document.getElementById('status-icon').addEventListener('click', function () {
|
||||
pingBackend();
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
// send cookie
|
||||
document.getElementById("sendCookies").addEventListener("click", function() {
|
||||
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");
|
||||
console.log('popup send cookie');
|
||||
clearError();
|
||||
|
||||
function handleResponse(message) {
|
||||
console.log("handle cookie response: " + JSON.stringify(message));
|
||||
console.log('handle cookie response: ' + JSON.stringify(message));
|
||||
let cookie_validated = message.cookie_validated;
|
||||
document.getElementById("sendCookiesStatus").innerText = "validated: " + cookie_validated
|
||||
document.getElementById('sendCookiesStatus').innerText = 'validated: ' + cookie_validated;
|
||||
}
|
||||
|
||||
function handleError(error) {
|
||||
console.log(`Error: ${error}`);
|
||||
setError(error);
|
||||
}
|
||||
|
||||
let checked = document.getElementById("sendCookies").checked;
|
||||
let checked = document.getElementById('sendCookies').checked;
|
||||
let toStore = {
|
||||
"sendCookies": {
|
||||
"checked": checked
|
||||
}
|
||||
sendCookies: {
|
||||
checked: checked,
|
||||
},
|
||||
};
|
||||
browserType.storage.local.set(toStore, function() {
|
||||
console.log("stored option: " + JSON.stringify(toStore));
|
||||
})
|
||||
browserType.storage.local.set(toStore, function () {
|
||||
console.log('stored option: ' + JSON.stringify(toStore));
|
||||
});
|
||||
if (checked === false) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
let sending = browserType.runtime.sendMessage({"sendCookie": true});
|
||||
let sending = sendMessage({ type: '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() {
|
||||
|
||||
function handleResponse(message) {
|
||||
if (message.response === "pong") {
|
||||
clearError();
|
||||
clearTempLocalStorage();
|
||||
function handleResponse() {
|
||||
console.log('connection validated');
|
||||
setStatusIcon(true);
|
||||
console.log("connection validated")
|
||||
}
|
||||
}
|
||||
|
||||
function handleError(error) {
|
||||
console.log(`Error: ${error}`);
|
||||
console.log(`Verify got error: ${error}`);
|
||||
setStatusIcon(false);
|
||||
setError(error);
|
||||
}
|
||||
|
||||
console.log("ping TA server")
|
||||
let sending = browserType.runtime.sendMessage({"verify": true});
|
||||
console.log('ping TA server');
|
||||
let sending = sendMessage({ type: 'verify' });
|
||||
sending.then(handleResponse, handleError);
|
||||
|
||||
}
|
||||
|
||||
// add url to image
|
||||
function addUrl(access) {
|
||||
const url = `${access.url}:${access.port}`;
|
||||
document.getElementById("ta-url").setAttribute("href", url);
|
||||
document.getElementById('ta-url').setAttribute('href', url);
|
||||
}
|
||||
|
||||
|
||||
function setCookieState() {
|
||||
|
||||
clearError();
|
||||
function handleResponse(message) {
|
||||
console.log(message);
|
||||
document.getElementById("sendCookies").checked = message.cookie_enabled;
|
||||
document.getElementById('sendCookies').checked = message.cookie_enabled;
|
||||
if (message.validated_str) {
|
||||
document.getElementById("sendCookiesStatus").innerText = message.validated_str;
|
||||
document.getElementById('sendCookiesStatus').innerText = message.validated_str;
|
||||
}
|
||||
}
|
||||
|
||||
function handleError(error) {
|
||||
console.log(`Error: ${error}`);
|
||||
setError(error);
|
||||
}
|
||||
|
||||
console.log("set cookie state");
|
||||
let sending = browserType.runtime.sendMessage({"cookieState": true});
|
||||
sending.then(handleResponse, handleError)
|
||||
document.getElementById("sendCookies").checked = true;
|
||||
console.log('set cookie state');
|
||||
let sending = sendMessage({ type: 'cookieState' });
|
||||
sending.then(handleResponse, handleError);
|
||||
document.getElementById('sendCookies').checked = true;
|
||||
}
|
||||
|
||||
|
||||
// change status icon based on connection status
|
||||
function setStatusIcon(connected) {
|
||||
|
||||
let statusIcon = document.getElementById("status-icon")
|
||||
if (connected == true) {
|
||||
statusIcon.innerHTML = "☑";
|
||||
statusIcon.style.color = "green";
|
||||
let statusIcon = document.getElementById('status-icon');
|
||||
if (connected) {
|
||||
statusIcon.innerHTML = '☑';
|
||||
statusIcon.style.color = 'green';
|
||||
} else {
|
||||
statusIcon.innerHTML = "☒";
|
||||
statusIcon.style.color = "red";
|
||||
statusIcon.innerHTML = '☒';
|
||||
statusIcon.style.color = 'red';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
function downloadEvent() {
|
||||
|
||||
let button = document.getElementById("downloadButton");
|
||||
let payload = {
|
||||
"download": {
|
||||
"url": button.getAttribute("data-id")
|
||||
}
|
||||
};
|
||||
|
||||
function handleResponse(message) {
|
||||
console.log("popup.js response: " + JSON.stringify(message));
|
||||
browserType.storage.local.remove("youtube").then(response => {
|
||||
let download = document.getElementById("download");
|
||||
download.innerHTML = ""
|
||||
let message = document.createElement("p");
|
||||
message.innerText = "Download link sent to Tube Archivist"
|
||||
download.appendChild(message)
|
||||
download.appendChild(document.createElement("hr"));
|
||||
})
|
||||
}
|
||||
|
||||
function handleError(error) {
|
||||
console.log(`Error: ${error}`);
|
||||
}
|
||||
|
||||
let sending = browserType.runtime.sendMessage(payload);
|
||||
sending.then(handleResponse, handleError)
|
||||
|
||||
}
|
||||
|
||||
|
||||
function subscribeEvent() {
|
||||
|
||||
let button = document.getElementById("subscribeButton");
|
||||
let payload = {
|
||||
"subscribe": {
|
||||
"url": button.getAttribute("data-id")
|
||||
}
|
||||
};
|
||||
|
||||
function handleResponse(message) {
|
||||
console.log("popup.js response: " + JSON.stringify(message));
|
||||
browserType.storage.local.remove("youtube").then(response => {
|
||||
let download = document.getElementById("download");
|
||||
download.innerHTML = ""
|
||||
let message = document.createElement("p");
|
||||
message.innerText = "Subscribe link sent to Tube Archivist"
|
||||
download.appendChild(message)
|
||||
download.appendChild(document.createElement("hr"));
|
||||
})
|
||||
}
|
||||
|
||||
function handleError(error) {
|
||||
console.log(`Error: ${error}`);
|
||||
}
|
||||
|
||||
let sending = browserType.runtime.sendMessage(payload);
|
||||
sending.then(handleResponse, handleError)
|
||||
|
||||
}
|
||||
|
||||
|
||||
// fill in form
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
function onGot(item) {
|
||||
if (!item.access) {
|
||||
console.log("no access details found");
|
||||
setStatusIcon(false);
|
||||
return
|
||||
console.log('no access details found');
|
||||
if (item.popupFullUrl != null && fullUrlInput.value === '') {
|
||||
fullUrlInput.value = item.popupFullUrl;
|
||||
}
|
||||
document.getElementById("url").value = item.access.url;
|
||||
document.getElementById("port").value = item.access.port;
|
||||
document.getElementById("api-key").value = item.access.apiKey;
|
||||
if (item.popupApiKey != null && apiKeyInput.value === '') {
|
||||
apiKeyInput.value = item.popupApiKey;
|
||||
}
|
||||
setStatusIcon(false);
|
||||
return;
|
||||
}
|
||||
let { url, port } = item.access;
|
||||
let fullUrl = url;
|
||||
if (!(url.startsWith('http://') && port === '80')) {
|
||||
fullUrl += `:${port}`;
|
||||
}
|
||||
document.getElementById('full-url').value = fullUrl;
|
||||
document.getElementById('api-key').value = item.access.apiKey;
|
||||
pingBackend();
|
||||
addUrl(item.access);
|
||||
};
|
||||
}
|
||||
|
||||
function setCookiesOptions(result) {
|
||||
if (!result.sendCookies || result.sendCookies.checked === false) {
|
||||
console.log("sync cookies not set");
|
||||
return
|
||||
console.log('sync cookies not set');
|
||||
return;
|
||||
}
|
||||
console.log("set options: " + JSON.stringify(result));
|
||||
console.log('set options: ' + JSON.stringify(result));
|
||||
setCookieState();
|
||||
|
||||
}
|
||||
|
||||
function onError(error) {
|
||||
console.log(`Error: ${error}`);
|
||||
};
|
||||
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", function(result) {
|
||||
onGot(result)
|
||||
browserType.storage.local.get(['access', 'popupFullUrl', 'popupApiKey'], function (result) {
|
||||
onGot(result);
|
||||
});
|
||||
|
||||
browserType.storage.local.get("sendCookies", function(result) {
|
||||
setCookiesOptions(result)
|
||||
})
|
||||
browserType.storage.local.get('sendCookies', function (result) {
|
||||
setCookiesOptions(result);
|
||||
});
|
||||
|
||||
browserType.storage.local.get("youtube", function(result) {
|
||||
if (result.youtube) {
|
||||
createButtons(result);
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
|
||||
function createButtons(result) {
|
||||
|
||||
let download = document.getElementById("download");
|
||||
let linkType = document.createElement("h3");
|
||||
linkType.innerText = result.youtube.type.charAt(0).toUpperCase() + result.youtube.type.slice(1);
|
||||
let title = document.createElement("p");
|
||||
title.innerText = result.youtube.title;
|
||||
|
||||
// dl button
|
||||
let downloadButton = document.createElement("button");
|
||||
downloadButton.innerText = "download";
|
||||
downloadButton.id = "downloadButton";
|
||||
downloadButton.setAttribute("data-id", result.youtube.url);
|
||||
downloadButton.addEventListener("click", function(){downloadEvent()}, false);
|
||||
|
||||
// subscribe button
|
||||
let subscribeButton = document.createElement("button");
|
||||
subscribeButton.innerText = "subscribe";
|
||||
subscribeButton.id = "subscribeButton";
|
||||
subscribeButton.setAttribute("data-id", result.youtube.url);
|
||||
subscribeButton.addEventListener("click", function(){subscribeEvent()}, false);
|
||||
|
||||
download.appendChild(linkType);
|
||||
download.appendChild(title);
|
||||
download.appendChild(downloadButton);
|
||||
download.appendChild(subscribeButton);
|
||||
download.appendChild(document.createElement("hr"));
|
||||
|
||||
}
|
||||
browserType.storage.local.get('autostart', function (result) {
|
||||
setAutostartOption(result);
|
||||
});
|
||||
});
|
||||
|
@ -2,74 +2,596 @@
|
||||
content script running on youtube.com
|
||||
*/
|
||||
|
||||
let browserType = getBrowser();
|
||||
'use strict';
|
||||
|
||||
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">
|
||||
.st0{display:none;}
|
||||
.st1{display:inline;}
|
||||
</style>
|
||||
<g class="st0">
|
||||
<g class="st1">
|
||||
<g>
|
||||
<rect x="49.8" y="437.8" width="400.4" height="32.4"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M49.8,193c2-9.4,7.6-16.4,14.5-22.6c2.9-2.6,5.5-5.5,8.3-8.3c13.1-12.9,31.6-13,44.6,0c23,22.9,45.9,45.9,68.8,68.8
|
||||
c0.7,0.7,1.5,1.4,2.5,2.4c1.1-1.1,2.2-2.1,3.3-3.1c63.4-63.4,126.8-126.8,190.2-190.2c10.7-10.7,24.6-13.3,37.1-6.7
|
||||
c2.9,1.6,5.6,3.8,8.1,6c4.2,3.9,8.2,8.1,12.2,12.1c14.3,14.3,14.3,32.4,0.1,46.6c-20.2,20.3-40.5,40.5-60.8,60.8
|
||||
C321,216.8,263.2,274.6,205.4,332.4c-11.2,11.2-22.4,11.2-33.6,0c-35.7-35.7-71.4-71.6-107.3-107.2
|
||||
c-6.7-6.6-12.7-13.4-14.8-22.8C49.8,199.2,49.8,196.1,49.8,193z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="237.9" y="313.5" transform="matrix(-1.836970e-16 1 -1 -1.836970e-16 708.0891 208.8956)" width="23.4" height="289.9"/>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M190.6,195.1c-21.7,0-42.5,0.1-63.4,0c-8.2,0-14.4,3-17.8,10.6c-3.5,7.9-1.3,14.6,4.5,20.7
|
||||
c40.6,42.4,81,84.9,121.6,127.3c8.9,9.3,19.1,9.4,28,0.1c40.7-42.5,81.3-85.1,122-127.7c5.6-5.9,7.6-12.6,4.3-20.3
|
||||
c-3.3-7.6-9.5-10.8-17.7-10.7c-19,0.1-38,0-57,0c-2,0-3.9,0-6.5,0c0-2.8,0-5,0-7.1c0-42.3,0.1-84.5,0-126.8
|
||||
c0-19.4-12.1-31.3-31.5-31.4c-17.9-0.1-35.8,0-53.7,0c-21.2,0-32.7,11.6-32.7,32.9c0,41.7,0,83.4,0,125.1
|
||||
C190.6,190,190.6,192.2,190.6,195.1z"/>
|
||||
<path d="M190.6,195.1c0-2.9,0-5.1,0-7.3c0-41.7,0-83.4,0-125.1c0-21.3,11.5-32.9,32.7-32.9c17.9,0,35.8-0.1,53.7,0
|
||||
c19.4,0.1,31.5,12,31.5,31.4c0.1,42.3,0,84.5,0,126.8c0,2.2,0,4.4,0,7.1c2.5,0,4.5,0,6.5,0c19,0,38,0.1,57,0
|
||||
c8.2,0,14.4,3.1,17.7,10.7c3.4,7.6,1.3,14.4-4.3,20.3c-40.7,42.6-81.3,85.2-122,127.7c-8.8,9.2-19.1,9.2-28-0.1
|
||||
c-40.5-42.4-81-84.9-121.6-127.3c-5.8-6.1-8-12.8-4.5-20.7c3.4-7.6,9.6-10.7,17.8-10.6C148.1,195.2,168.9,195.1,190.6,195.1z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>`;
|
||||
|
||||
const checkmarkIcon = `<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">
|
||||
.st0{display:none;}
|
||||
.st1{display:inline;}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<rect x="49.8" y="437.8" width="400.4" height="32.4"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M49.8,193c2-9.4,7.6-16.4,14.5-22.6c2.9-2.6,5.5-5.5,8.3-8.3c13.1-12.9,31.6-13,44.6,0c23,22.9,45.9,45.9,68.8,68.8
|
||||
c0.7,0.7,1.5,1.4,2.5,2.4c1.1-1.1,2.2-2.1,3.3-3.1c63.4-63.4,126.8-126.8,190.2-190.2c10.7-10.7,24.6-13.3,37.1-6.7
|
||||
c2.9,1.6,5.6,3.8,8.1,6c4.2,3.9,8.2,8.1,12.2,12.1c14.3,14.3,14.3,32.4,0.1,46.6c-20.2,20.3-40.5,40.5-60.8,60.8
|
||||
C321,216.8,263.2,274.6,205.4,332.4c-11.2,11.2-22.4,11.2-33.6,0c-35.7-35.7-71.4-71.6-107.3-107.2
|
||||
c-6.7-6.6-12.7-13.4-14.8-22.8C49.8,199.2,49.8,196.1,49.8,193z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g class="st0">
|
||||
|
||||
<rect x="237.9" y="313.5" transform="matrix(-1.836970e-16 1 -1 -1.836970e-16 708.0891 208.8956)" class="st1" width="23.4" height="289.9"/>
|
||||
<g class="st1">
|
||||
<g>
|
||||
<path d="M190.6,195.1c-21.7,0-42.5,0.1-63.4,0c-8.2,0-14.4,3-17.8,10.6c-3.5,7.9-1.3,14.6,4.5,20.7
|
||||
c40.6,42.4,81,84.9,121.6,127.3c8.9,9.3,19.1,9.4,28,0.1c40.7-42.5,81.3-85.1,122-127.7c5.6-5.9,7.6-12.6,4.3-20.3
|
||||
c-3.3-7.6-9.5-10.8-17.7-10.7c-19,0.1-38,0-57,0c-2,0-3.9,0-6.5,0c0-2.8,0-5,0-7.1c0-42.3,0.1-84.5,0-126.8
|
||||
c0-19.4-12.1-31.3-31.5-31.4c-17.9-0.1-35.8,0-53.7,0c-21.2,0-32.7,11.6-32.7,32.9c0,41.7,0,83.4,0,125.1
|
||||
C190.6,190,190.6,192.2,190.6,195.1z"/>
|
||||
<path d="M190.6,195.1c0-2.9,0-5.1,0-7.3c0-41.7,0-83.4,0-125.1c0-21.3,11.5-32.9,32.7-32.9c17.9,0,35.8-0.1,53.7,0
|
||||
c19.4,0.1,31.5,12,31.5,31.4c0.1,42.3,0,84.5,0,126.8c0,2.2,0,4.4,0,7.1c2.5,0,4.5,0,6.5,0c19,0,38,0.1,57,0
|
||||
c8.2,0,14.4,3.1,17.7,10.7c3.4,7.6,1.3,14.4-4.3,20.3c-40.7,42.6-81.3,85.2-122,127.7c-8.8,9.2-19.1,9.2-28-0.1
|
||||
c-40.5-42.4-81-84.9-121.6-127.3c-5.8-6.1-8-12.8-4.5-20.7c3.4-7.6,9.6-10.7,17.8-10.6C148.1,195.2,168.9,195.1,190.6,195.1z"/>
|
||||
</g>
|
||||
</g>
|
||||
</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");
|
||||
if (typeof chrome !== 'undefined') {
|
||||
if (typeof browser !== 'undefined') {
|
||||
console.log('detected firefox');
|
||||
return browser;
|
||||
} else {
|
||||
console.log("detected chrome");
|
||||
console.log('detected chrome');
|
||||
return chrome;
|
||||
}
|
||||
} else {
|
||||
console.log("failed to dedect browser");
|
||||
throw "browser detection error"
|
||||
console.log('failed to dedect browser');
|
||||
throw 'browser detection error';
|
||||
}
|
||||
}
|
||||
|
||||
function getChannelContainers() {
|
||||
const elements = document.querySelectorAll('#inner-header-container, #owner');
|
||||
const channelContainerNodes = [];
|
||||
|
||||
elements.forEach(element => {
|
||||
if (isElementVisible(element)) {
|
||||
channelContainerNodes.push(element);
|
||||
}
|
||||
});
|
||||
|
||||
return channelContainerNodes;
|
||||
}
|
||||
|
||||
function isElementVisible(element) {
|
||||
return element.offsetWidth > 0 || element.offsetHeight > 0 || element.getClientRects().length > 0;
|
||||
}
|
||||
|
||||
function ensureTALinks() {
|
||||
let channelContainerNodes = getChannelContainers();
|
||||
|
||||
for (let channelContainer of channelContainerNodes) {
|
||||
channelContainer = adjustOwner(channelContainer);
|
||||
if (channelContainer.hasTA) continue;
|
||||
let channelButton = buildChannelButton(channelContainer);
|
||||
channelContainer.appendChild(channelButton);
|
||||
channelContainer.hasTA = true;
|
||||
}
|
||||
|
||||
let titleContainerNodes = getTitleContainers();
|
||||
for (let titleContainer of titleContainerNodes) {
|
||||
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('#channel-handle');
|
||||
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');
|
||||
}
|
||||
e.stopPropagation();
|
||||
});
|
||||
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);
|
||||
e.stopPropagation();
|
||||
});
|
||||
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) {
|
||||
let buttonSpan = button.querySelector('span');
|
||||
if (buttonSpan === null) {
|
||||
buttonSpan = button;
|
||||
}
|
||||
buttonSpan.style.filter =
|
||||
'invert(19%) sepia(93%) saturate(7472%) hue-rotate(359deg) brightness(105%) contrast(113%)';
|
||||
buttonSpan.style.color = 'red';
|
||||
|
||||
button.style.opacity = 1;
|
||||
button.addEventListener('mouseout', () => {
|
||||
Object.assign(button.style, {
|
||||
opacity: 1,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function buttonSuccess(button) {
|
||||
let buttonSpan = button.querySelector('span');
|
||||
if (buttonSpan === null) {
|
||||
buttonSpan = button;
|
||||
}
|
||||
if (buttonSpan.innerHTML === 'Subscribe') {
|
||||
buttonSpan.innerHTML = 'Success';
|
||||
setTimeout(() => {
|
||||
buttonSpan.innerHTML = 'Unsubscribe';
|
||||
}, 2000);
|
||||
} else {
|
||||
buttonSpan.innerHTML = checkmarkIcon;
|
||||
}
|
||||
}
|
||||
|
||||
function sendUrl(url, action, button) {
|
||||
function handleResponse(message) {
|
||||
console.log('sendUrl response: ' + JSON.stringify(message));
|
||||
if (message === null || message.detail === 'Invalid token.') {
|
||||
buttonError(button);
|
||||
} else {
|
||||
buttonSuccess(button);
|
||||
}
|
||||
}
|
||||
|
||||
function handleError(e) {
|
||||
console.log('error', e);
|
||||
buttonError(button);
|
||||
}
|
||||
|
||||
let message = { type: action, url };
|
||||
|
||||
console.log('youtube link: ' + JSON.stringify(message));
|
||||
|
||||
let sending = sendMessage(message);
|
||||
sending.then(handleResponse, handleError);
|
||||
}
|
||||
|
||||
async function sendMessage(message) {
|
||||
let { success, value } = await browserType.runtime.sendMessage(message);
|
||||
if (!success) {
|
||||
throw value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function cleanButtons() {
|
||||
console.log('trigger clean buttons');
|
||||
document.querySelectorAll('.ta-button').forEach(button => {
|
||||
button.parentElement.hasTA = false;
|
||||
button.remove();
|
||||
});
|
||||
document.querySelectorAll('.ta-channel-button').forEach(button => {
|
||||
button.parentElement.hasTA = false;
|
||||
button.remove();
|
||||
});
|
||||
}
|
||||
|
||||
let oldHref = document.location.href;
|
||||
|
||||
function throttled(callback, time) {
|
||||
let throttleBlock = false;
|
||||
let lastArgs;
|
||||
return (...args) => {
|
||||
lastArgs = args;
|
||||
if (throttleBlock) return;
|
||||
throttleBlock = true;
|
||||
setTimeout(() => {
|
||||
throttleBlock = false;
|
||||
callback(...lastArgs);
|
||||
}, time);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function detectUrlType(url) {
|
||||
|
||||
const videoRe = new RegExp(/^https:\/\/(www\.)?(youtube.com\/watch\?v=|youtu\.be\/)[\w-]{11}/);
|
||||
if (videoRe.test(url)) {
|
||||
return "video"
|
||||
let observer = new MutationObserver(list => {
|
||||
const currentHref = document.location.href;
|
||||
if (currentHref !== oldHref) {
|
||||
cleanButtons();
|
||||
oldHref = currentHref;
|
||||
}
|
||||
const channelRe = new RegExp(/^https:?\/\/www\.?youtube.com\/c|channel|user\/[\w-]+(\/|featured|videos)?$/);
|
||||
if (channelRe.test(url)) {
|
||||
return "channel"
|
||||
if (list.some(i => i.type === 'childList' && i.addedNodes.length > 0)) {
|
||||
ensureTALinks();
|
||||
}
|
||||
const playlistRe = new RegExp(/^https:\/\/(www\.)?youtube.com\/playlist\?list=/);
|
||||
if (playlistRe.test(url)) {
|
||||
return "playlist"
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
|
||||
function sendUrl() {
|
||||
|
||||
let url = document.URL
|
||||
|
||||
let urlType = detectUrlType(url);
|
||||
if (urlType == false) {
|
||||
console.log("not relevant")
|
||||
return
|
||||
}
|
||||
|
||||
let payload = {
|
||||
"youtube": {
|
||||
"url": url,
|
||||
"title": document.title,
|
||||
"type": urlType,
|
||||
}
|
||||
}
|
||||
console.log("youtube link: " + JSON.stringify(payload));
|
||||
browserType.runtime.sendMessage(payload, function(response) {
|
||||
console.log(response.farewell);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
document.addEventListener("yt-navigate-finish", function (event) {
|
||||
setTimeout(function(){
|
||||
sendUrl();
|
||||
return false;
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
observer.observe(document.body, { attributes: false, childList: true, subtree: true });
|
||||
|
@ -5,8 +5,8 @@ body {
|
||||
}
|
||||
.container {
|
||||
padding: 10px;
|
||||
min-width: 300px;
|
||||
max-width: 400px;
|
||||
min-width: 350px;
|
||||
max-width: 450px;
|
||||
}
|
||||
.h3 {
|
||||
font-family: Sen-bold, sans-serif;
|
||||
@ -48,8 +48,7 @@ hr {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.submit button,
|
||||
.youtube-page button {
|
||||
.submit button {
|
||||
margin: 10px;
|
||||
border-radius: 0;
|
||||
padding: 5px 13px;
|
||||
@ -58,14 +57,13 @@ hr {
|
||||
background-color: #259485;
|
||||
color: #ffffff;
|
||||
}
|
||||
.submit button:hover,
|
||||
.youtube-page button:hover {
|
||||
.submit button:hover {
|
||||
background-color: #97d4c8;
|
||||
transform: scale(1.05);
|
||||
color: #00202f;
|
||||
}
|
||||
.options {
|
||||
display: flex;
|
||||
display: block;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.options span {
|
||||
@ -79,3 +77,7 @@ hr {
|
||||
.icons img {
|
||||
width: 25px;
|
||||
}
|
||||
#error-out {
|
||||
color: red;
|
||||
display: none; /* will be made visible when an error occurs */
|
||||
}
|
||||
|
1960
package-lock.json
generated
Normal file
18
package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "tubearchivist-browser-extension",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint 'extension/**/*.js'",
|
||||
"format": "prettier --write 'extension/**/*.js'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.26.0",
|
||||
"prettier": "^2.7.1",
|
||||
"eslint-config-prettier": "^8.5.0"
|
||||
},
|
||||
"prettier": {
|
||||
"singleQuote": true,
|
||||
"arrowParens": "avoid",
|
||||
"printWidth": 100
|
||||
}
|
||||
}
|