Skip to content

Commit 058dccc

Browse files
committed
Display "Watch on Nebula" on Indivious [#92]
1 parent 3eb5b69 commit 058dccc

6 files changed

Lines changed: 205 additions & 1 deletion

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"dependencies": {
5353
"dompurify": "^3.2.4",
5454
"marked": "^15.0.6",
55+
"webext-dynamic-content-scripts": "^10.0.4",
5556
"webextension-polyfill": "^0.12.0"
5657
},
5758
"devDependencies": {

pnpm-lock.yaml

Lines changed: 75 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/scripts/background_script.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'webext-dynamic-content-scripts';
12
import { Creator, loadCreators as _loadCreators, creatorHasNebulaVideo, creatorHasYTVideo, existsNebulaVideo, normalizeString } from './background';
23
import { purgeCache, purgeCacheIfNecessary } from './background/ext';
34
import type { CreatorSettings } from './content/nebula/creator-settings';
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { getBrowserInstance, nebulavideo } from '../../helpers/sharedExt';
2+
3+
const watchOnNebula = getBrowserInstance().i18n.getMessage('pageWatchOnNebula');
4+
const goChannel = getBrowserInstance().i18n.getMessage('pageGoChannel');
5+
const videoConfidence = getBrowserInstance().i18n.getMessage('pageVideoConfidence');
6+
const searchConfidence = getBrowserInstance().i18n.getMessage('pageSearchConfidence');
7+
8+
export const constructButton = (vid: nebulavideo, before: HTMLElement) => {
9+
if (!document.querySelector('.watch-on-nebula') || document.querySelector('.watch-on-nebula').children.length === 0) {
10+
Array.from(document.querySelectorAll<HTMLElement>('.watch-on-nebula')).forEach(n => n.remove());
11+
// for some reason youtube custom elements clear their inner html in construct, so we have to do it like this
12+
const wrap = document.createElement('p');
13+
wrap.style.display = 'none';
14+
before.before(wrap);
15+
wrap.className = 'watch-on-nebula';
16+
const button = wrap.appendChild(document.createElement('span'));
17+
button.id = 'watch-on-nebula';
18+
const blink = button.appendChild(document.createElement('a'));
19+
blink.id = 'link-nebula-watch';
20+
blink.rel = 'noopener noreferrer';
21+
blink.textContent = watchOnNebula;
22+
blink.setAttribute('href', vid.link);
23+
button.title = generateText(vid);
24+
} else {
25+
document.querySelector<HTMLSpanElement>('.watch-on-nebula a').setAttribute('href', vid.link);
26+
document.querySelector<HTMLButtonElement>('.watch-on-nebula span').title = generateText(vid);
27+
}
28+
const b = document.querySelector<HTMLElement>('.watch-on-nebula');
29+
b.style.display = '';
30+
return b;
31+
};
32+
33+
const generateText = (vid: nebulavideo) => {
34+
switch (vid.is) {
35+
case 'channel':
36+
return goChannel;
37+
case 'video':
38+
return `${videoConfidence}: ${(vid.confidence * 100).toFixed(1)}%`;
39+
case 'search':
40+
return `${searchConfidence}: ${(vid.confidence * 100).toFixed(1)}%`;
41+
}
42+
};
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { BrowserMessage, debounce, getBrowserInstance, getFromStorage, nebulavideo } from '../../helpers/sharedExt';
2+
import { constructButton } from './html';
3+
4+
const optionsDefaults = {
5+
ytOpenTab: false,
6+
ytMuteOnly: false,
7+
ytReplaceTab: false,
8+
};
9+
let options = { ...optionsDefaults };
10+
11+
export const invidious = async () => {
12+
options = await getFromStorage(optionsDefaults);
13+
console.debug('Invidious loaded on', location.hostname);
14+
getBrowserInstance().storage.onChanged.addListener(changed => {
15+
Object.keys(options).forEach(prop => {
16+
if (prop in changed && 'newValue' in changed[prop]) {
17+
options[prop] = changed[prop].newValue as typeof options[typeof prop];
18+
}
19+
});
20+
console.dev.debug('Reload information', changed);
21+
setTimeout(run, 100);
22+
});
23+
24+
Array.from(document.querySelectorAll<HTMLElement>('.watch-on-nebula')).forEach(n => n.remove());
25+
setTimeout(run, 0);
26+
};
27+
28+
const remove = () => Array.from(document.querySelectorAll<HTMLElement>('.watch-on-nebula')).forEach(n => n.style.display = 'none');
29+
const findParentLink = (el: HTMLElement) => {
30+
while (el) {
31+
if (el.tagName === 'A') return el as HTMLAnchorElement;
32+
el = el.parentElement;
33+
}
34+
};
35+
const run = debounce(async () => {
36+
if (!location.pathname.startsWith('/watch')) {
37+
console.dev.log('not a video');
38+
remove();
39+
return;
40+
}
41+
42+
const channelEl = document.getElementById('channel-name');
43+
const channelNice = channelEl?.textContent.trim();
44+
const channelLink = findParentLink(channelEl);
45+
const channelID = channelLink?.href.match(/\/channel\/([^\/]+)/)?.[1];
46+
const titleEl = document.querySelector('#player-container ~ * h1');
47+
const videoTitle = titleEl?.textContent.trim();
48+
console.dev.debug('Elements', !!channelEl, !!titleEl, !!channelLink);
49+
if (!channelEl || !titleEl || !channelLink) return;
50+
const vidID = location.search.match(/[?&]v=([^&]+)/)?.[1];
51+
52+
const { res: vid, err }: { res?: nebulavideo, err?: any; } = await getBrowserInstance().runtime.sendMessage({ type: BrowserMessage.GET_VID, channelID, channelName: channelNice, channelNice, videoTitle });
53+
console.dev.log('got:', vid, err);
54+
if (!vid) throw new Error(err);
55+
console.debug('got video information',
56+
'\nchannelID:', channelID, 'channelName:', channelNice, 'videoTitle:', videoTitle, 'vidID:', vidID,
57+
'\non nebula?', !!vid);
58+
59+
if (!vid) return remove();
60+
console.dev.log('Found video:', vid);
61+
62+
const watchOnYtEl = document.querySelector<HTMLElement>('#watch-on-youtube');
63+
if (!watchOnYtEl) return;
64+
constructButton(vid, watchOnYtEl);
65+
66+
if ((window.history.state || {})['_enhancer_checked'] === true) return console.debug('Ignoring video since already processed');
67+
68+
const { ytOpenTab: doOpenTab, ytReplaceTab: replaceTab } = options;
69+
console.dev.debug('Referer:', document.referrer);
70+
if (document.referrer.match(/https?:\/\/(.+\.)?nebula\.(app|tv)\/?/) && window.history.length <= 1) return; // prevent open link if via nebula link (any link)
71+
if (vid.is === 'channel' || !doOpenTab) return;
72+
73+
/* eslint-disable-next-line camelcase */
74+
window.history.replaceState({ ...window.history.state, _enhancer_checked: true }, '');
75+
76+
if (replaceTab) {
77+
window.location.href = vid.link;
78+
return;
79+
}
80+
81+
window.open(vid.link, vidID);
82+
}, 5);

src/scripts/content_script.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { invidious } from './content/invidious';
12
import { nebula } from './content/nebula';
23
import { youtube } from './content/youtube';
34
import { BrowserMessage, getBrowserInstance } from './helpers/sharedExt';
@@ -7,8 +8,10 @@ import { BrowserMessage, getBrowserInstance } from './helpers/sharedExt';
78

89
if (window.location.hostname.endsWith('youtube.com')) {
910
youtube();
10-
} else {
11+
} else if (window.location.hostname.endsWith('nebula.tv') || window.location.hostname.endsWith('nebula.app')) {
1112
nebula();
13+
} else {
14+
invidious();
1215
}
1316
})();
1417

0 commit comments

Comments
 (0)