better way to call background methods, rpcs

This commit is contained in:
Shiba 2022-12-09 15:01:28 +00:00
parent 8c26ae0872
commit 494305a78d
3 changed files with 152 additions and 96 deletions

View file

@ -22,7 +22,8 @@
"extensions": [
"bierner.folder-source-actions",
"jbockle.jbockle-format-files",
"eamodio.gitlens"
"eamodio.gitlens",
"GitHub.copilot"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],

View file

@ -1,36 +1,69 @@
import { resolveById } from "../modules/yt/urlResolve"
const onGoingLbryPathnameRequest: Record<string, ReturnType<typeof resolveById>> = {}
interface BackgroundMethod<P extends any[], R> {
PARAMS_TYPE: P
RESULT_TYPE: R
call(sender: chrome.runtime.MessageSender, paramsJson: string, ...params: P): Promise<R>
}
chrome.runtime.onMessage.addListener(({ method, data }, sender, sendResponse) => {
function resolve(result: Awaited<ReturnType<typeof resolveById>>) {
sendResponse(JSON.stringify(result))
// Satifies BackgroundMethod
function backgroundMethod<P extends any[], R>(method: BackgroundMethod<P, R>['call']): BackgroundMethod<P, R> {
return {
PARAMS_TYPE: null as any,
RESULT_TYPE: null as any,
call: method
}
(async () => {
}
switch (method) {
case 'openTab':
{
const { href }: { href: string } = JSON.parse(data)
chrome.tabs.create({ url: href, active: sender.tab?.active, index: sender.tab ? sender.tab.index + 1 : undefined })
}
break
case 'resolveUrl':
try {
const params: Parameters<typeof resolveById> = JSON.parse(data)
// Don't create a new Promise for same ID until on going one is over.
const promise = onGoingLbryPathnameRequest[data] ?? (onGoingLbryPathnameRequest[data] = resolveById(...params))
resolve(await promise)
} catch (error) {
sendResponse(`error: ${(error as any).toString()}`)
console.error(error)
}
finally {
delete onGoingLbryPathnameRequest[data]
}
break
const resolveUrlOnGoingRequest: Record<string, ReturnType<typeof resolveById>> = {}
export type BackgroundMethods = typeof methods
const methods = {
/*
This method is needed to open a new tab from a content script,
because using window.open() from a content script is blocked by Chrome while using redirect feature.
*/
openTab: backgroundMethod<[{ href: string }], void>(async (sender, json, { href }) => {
chrome.tabs.create({ url: href, active: sender.tab?.active, index: sender.tab ? sender.tab.index + 1 : undefined })
}),
/*
This method is needed to resolve a YouTube URL from a content script,
this is on the background script because so we can cache the result and avoid making multiple requests for the same ID.
*/
resolveUrlById: backgroundMethod<Parameters<typeof resolveById>, Awaited<ReturnType<typeof resolveById>>>(async (sender, json, ...params) => {
try {
// Don't create a new Promise for same ID until on going one is over.
const promise = resolveUrlOnGoingRequest[json] ?? (resolveUrlOnGoingRequest[json] = resolveById(...params))
return await promise
}
})()
catch (error) {
throw error
}
finally {
delete resolveUrlOnGoingRequest[json]
}
})
} as const
chrome.runtime.onMessage.addListener(({ method, data }: { method: keyof BackgroundMethods, data: string }, sender, sendResponse) => {
try {
const methodData = methods[method]
if (!methodData) throw new Error(`Unknown method: ${method}`)
methodData.call(sender, data, ...JSON.parse(data))
.then(result => sendResponse(JSON.stringify(result ?? null)))
}
catch (error) {
sendResponse(`error: ${error?.toString?.()}`)
console.error(error)
}
return true
})

View file

@ -1,19 +1,23 @@
import { h, render } from 'preact'
import { parseYouTubeURLTimeString } from '../modules/yt'
import type { resolveById, ResolveUrlTypes } from '../modules/yt/urlResolve'
import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTargetPlatfromSettingsEntiries, SourcePlatform, sourcePlatfromSettings, TargetPlatform, targetPlatformSettings } from '../settings';
import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTargetPlatfromSettingsEntiries, SourcePlatform, sourcePlatfromSettings, TargetPlatform, targetPlatformSettings } from '../settings'
import type { BackgroundMethods } from './background'
(async () => {
(async () =>
{
const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t))
interface Target {
interface Target
{
platform: TargetPlatform
lbryPathname: string
type: ResolveUrlTypes
time: number | null
}
interface Source {
interface Source
{
platform: SourcePlatform
id: string
type: ResolveUrlTypes
@ -23,19 +27,27 @@ import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTa
const targetPlatforms = getTargetPlatfromSettingsEntiries()
const settings = await getExtensionSettingsAsync()
// Listen Settings Change
chrome.storage.onChanged.addListener(async (changes, areaName) => {
chrome.storage.onChanged.addListener(async (changes, areaName) =>
{
if (areaName !== 'local') return
Object.assign(settings, Object.fromEntries(Object.entries(changes).map(([key, change]) => [key, change.newValue])))
})
async function callBackgroundMethod<T extends keyof BackgroundMethods>(method: T, ...params: BackgroundMethods[T]['PARAMS_TYPE']): Promise<BackgroundMethods[T]['RESULT_TYPE']>
{
const response = await new Promise<string>((resolve) => chrome.runtime.sendMessage({ method, data: JSON.stringify(params) }, resolve))
if (response.startsWith('error:')) throw new Error(`Background Error: ${response.substring('error:'.length).trim()}`)
return JSON.parse(response)
}
const buttonMountPoint = document.createElement('div')
buttonMountPoint.style.display = 'inline-flex'
const playerButtonMountPoint = document.createElement('div')
playerButtonMountPoint.style.display = 'inline-flex'
function WatchOnLbryButton({ source, target }: { source?: Source, target?: Target }) {
function WatchOnLbryButton({ source, target }: { source?: Source, target?: Target })
{
if (!target || !source) return null
const url = getLbryUrlByTarget(target)
@ -65,7 +77,8 @@ import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTa
backgroundImage: target.platform.theme,
...target.platform.button.style?.button,
}}
onClick={() => findVideoElementAwait(source).then((videoElement) => {
onClick={() => findVideoElementAwait(source).then((videoElement) =>
{
videoElement.pause()
})}
>
@ -75,7 +88,8 @@ import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTa
</div>
}
function WatchOnLbryPlayerButton({ source, target }: { source?: Source, target?: Target }) {
function WatchOnLbryPlayerButton({ source, target }: { source?: Source, target?: Target })
{
if (!target || !source) return null
const url = getLbryUrlByTarget(target)
@ -102,7 +116,8 @@ import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTa
textDecoration: 'none',
...target.platform.button.style?.button,
}}
onClick={() => findVideoElementAwait(source).then((videoElement) => {
onClick={() => findVideoElementAwait(source).then((videoElement) =>
{
videoElement.pause()
})}
>
@ -112,8 +127,10 @@ import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTa
</div>
}
function updateButtons(params: { source: Source, target: Target } | null): void {
if (!params) {
function updateButtons(params: { source: Source, target: Target } | null): void
{
if (!params)
{
render(<WatchOnLbryButton />, buttonMountPoint)
render(<WatchOnLbryPlayerButton />, playerButtonMountPoint)
return
@ -124,8 +141,10 @@ import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTa
document.querySelector(params.source.platform.htmlQueries.mountPoints.mountPlayerButtonBefore) :
null
if (!mountPlayerButtonBefore) render(<WatchOnLbryPlayerButton />, playerButtonMountPoint)
else {
if (playerButtonMountPoint.getAttribute('data-id') !== params.source.id) {
else
{
if (playerButtonMountPoint.getAttribute('data-id') !== params.source.id)
{
mountPlayerButtonBefore.parentElement?.insertBefore(playerButtonMountPoint, mountPlayerButtonBefore)
playerButtonMountPoint.setAttribute('data-id', params.source.id)
}
@ -138,8 +157,10 @@ import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTa
document.querySelector(params.source.platform.htmlQueries.mountPoints.mountButtonBefore[params.source.type]) :
null
if (!mountButtonBefore) render(<WatchOnLbryButton />, buttonMountPoint)
else {
if (buttonMountPoint.getAttribute('data-id') !== params.source.id) {
else
{
if (buttonMountPoint.getAttribute('data-id') !== params.source.id)
{
mountButtonBefore.parentElement?.insertBefore(buttonMountPoint, mountButtonBefore)
buttonMountPoint.setAttribute('data-id', params.source.id)
}
@ -148,17 +169,20 @@ import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTa
}
}
async function findVideoElementAwait(source: Source) {
async function findVideoElementAwait(source: Source)
{
let videoElement: HTMLVideoElement | null = null
while (!(videoElement = document.querySelector(source.platform.htmlQueries.videoPlayer))) await sleep(200)
return videoElement
}
async function getSourceByUrl(url: URL): Promise<Source | null> {
async function getSourceByUrl(url: URL): Promise<Source | null>
{
const platform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname)
if (!platform) return null
if (url.pathname === '/watch' && url.searchParams.has('v')) {
if (url.pathname === '/watch' && url.searchParams.has('v'))
{
return {
id: url.searchParams.get('v')!,
platform,
@ -167,7 +191,8 @@ import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTa
url
}
}
else if (url.pathname.startsWith('/channel/')) {
else if (url.pathname.startsWith('/channel/'))
{
return {
id: url.pathname.substring("/channel/".length),
platform,
@ -176,7 +201,8 @@ import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTa
url
}
}
else if (url.pathname.startsWith('/c/') || url.pathname.startsWith('/user/')) {
else if (url.pathname.startsWith('/c/') || url.pathname.startsWith('/user/'))
{
// We have to download the page content again because these parts of the page are not responsive
// yt front end sucks anyway
const content = await (await fetch(location.href)).text()
@ -197,13 +223,13 @@ import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTa
return null
}
async function getTargetsBySources(...sources: Source[]) {
const params: Parameters<typeof requestResolveById>[0] = sources.map((source) => ({ id: source.id, type: source.type }))
async function getTargetsBySources(...sources: Source[])
{
const platform = targetPlatformSettings[settings.targetPlatform]
const results = await requestResolveById(params) ?? []
const results = await callBackgroundMethod('resolveUrlById', sources.map((source) => ({ id: source.id, type: source.type })))
const targets: Record<string, Target | null> = Object.fromEntries(
sources.map((source) => {
sources.map((source) =>
{
const result = results[source.id]
if (!result) return [
source.id,
@ -224,47 +250,39 @@ import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTa
return targets
}
// We should get this from background, so the caching works and we don't get errors in the future if yt decides to impliment CORS
async function requestResolveById(...params: Parameters<typeof resolveById>): ReturnType<typeof resolveById> {
const response = await new Promise<string | null | 'error'>((resolve) => chrome.runtime.sendMessage({ method: 'resolveUrl', data: JSON.stringify(params) }, resolve))
if (response?.startsWith('error:')) {
console.error("Background error on:", params)
throw new Error(`Background error. ${response ?? ''}`)
}
return response ? JSON.parse(response) : null
}
// Request new tab
async function openNewTab(url: URL) {
chrome.runtime.sendMessage({ method: 'openTab', data: JSON.stringify({ href: url.href }) })
}
function findTargetFromSourcePage(source: Source): Target | null {
function findTargetFromSourcePage(source: Source): Target | null
{
const linksContainer =
source.type === 'video' ?
document.querySelector(source.platform.htmlQueries.videoDescription) :
source.platform.htmlQueries.channelLinks ? document.querySelector(source.platform.htmlQueries.channelLinks) : null
if (linksContainer) {
if (linksContainer)
{
const anchors = Array.from(linksContainer.querySelectorAll<HTMLAnchorElement>('a'))
for (const anchor of anchors) {
for (const anchor of anchors)
{
if (!anchor.href) continue
const url = new URL(anchor.href)
let lbryURL: URL | null = null
// Extract real link from youtube's redirect link
if (source.platform === sourcePlatfromSettings['youtube.com']) {
if (source.platform === sourcePlatfromSettings['youtube.com'])
{
if (!targetPlatforms.some(([key, platform]) => url.searchParams.get('q')?.startsWith(platform.domainPrefix))) continue
lbryURL = new URL(url.searchParams.get('q')!)
}
// Just directly use the link itself on other platforms
else {
else
{
if (!targetPlatforms.some(([key, platform]) => url.href.startsWith(platform.domainPrefix))) continue
lbryURL = new URL(url.href)
}
if (lbryURL) {
if (lbryURL)
{
return {
lbryPathname: lbryURL.pathname.substring(1),
time: null,
@ -277,31 +295,28 @@ import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTa
return null
}
function getLbryUrlByTarget(target: Target) {
function getLbryUrlByTarget(target: Target)
{
const url = new URL(`${target.platform.domainPrefix}${target.lbryPathname}`)
if (target.time) url.searchParams.set('t', target.time.toFixed(0))
return url
}
// Master Loop
for (
let url = new URL(location.href),
urlHrefCache: string | null = null;
;
urlHrefCache = url.href,
url = new URL(location.href)
) {
for (let url = new URL(location.href), urlHrefCache: string | null = null; ; urlHrefCache = url.href, url = new URL(location.href))
{
await sleep(500)
try {
try
{
const source = await getSourceByUrl(new URL(location.href))
if (!source) {
if (!source)
{
updateButtons(null)
continue
}
const target = (await getTargetsBySources(source))[source.id] ?? findTargetFromSourcePage(source)
if (!target) {
if (!target)
{
updateButtons(null)
continue
}
@ -309,7 +324,8 @@ import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTa
// Update Buttons
if (urlHrefCache !== url.href) updateButtons(null)
// If target is a video target add timestampt to it
if (target.type === 'video') {
if (target.type === 'video')
{
const videoElement = document.querySelector<HTMLVideoElement>(source.platform.htmlQueries.videoPlayer)
if (videoElement) target.time = videoElement.currentTime > 3 && videoElement.currentTime < videoElement.duration - 1 ? videoElement.currentTime : null
}
@ -332,30 +348,36 @@ import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTa
source.type === 'channel'
)
)
) {
)
{
if (url.href === urlHrefCache) continue
const lbryURL = getLbryUrlByTarget(target)
if (source.type === 'video') {
if (source.type === 'video')
{
findVideoElementAwait(source).then((videoElement) => videoElement.pause())
}
if (target.platform === targetPlatformSettings.app) {
if (target.platform === targetPlatformSettings.app)
{
if (document.hidden) await new Promise((resolve) => document.addEventListener('visibilitychange', resolve, { once: true }))
// Replace is being used so browser doesnt start an empty window
// Its not gonna be able to replace anyway, since its a LBRY Uri
location.replace(lbryURL)
}
else {
openNewTab(lbryURL)
if (window.history.length === 1)
else
{
callBackgroundMethod('openTab', { href: lbryURL.href })
if (window.history.length === 1)
window.close()
else
else
window.history.back()
}
}
} catch (error) {
}
catch (error)
{
console.error(error)
}
}