This commit is contained in:
Shiba 2022-12-09 22:53:42 +00:00 committed by GitHub
commit ab66b062a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 158 additions and 10180 deletions

View file

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

View file

@ -1,6 +1,8 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
# Also uses `python2` to build the webextension
name: Node.js CI name: Node.js CI
on: on:
@ -24,7 +26,10 @@ jobs:
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- run: yarn - name: Install dependencies
run: |
sudo apt-get install python2
yarn
- run: npm run build - run: npm run build
- run: npm run - run: npm run
- run: npm run build:webext - run: npm run build:webext

View file

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

View file

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

10083
yarn.lock

File diff suppressed because it is too large Load diff