Merge pull request #127 from DeepDoge/master

Watch on LBRY button also added to the Video player + Some bug fixes
This commit is contained in:
kodxana 2022-07-25 22:47:40 +02:00 committed by GitHub
commit 0aa64bb8b2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 196 additions and 108 deletions

View file

@ -13,10 +13,14 @@ import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTa
time: number | null time: number | null
} }
const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname) interface Source {
if (!sourcePlatform) return platform: SourcePlatform
const targetPlatforms = getTargetPlatfromSettingsEntiries() id: string
type: ResolveUrlTypes
time: number | null
}
const targetPlatforms = getTargetPlatfromSettingsEntiries()
const settings = await getExtensionSettingsAsync() const settings = await getExtensionSettingsAsync()
// Listen Settings Change // Listen Settings Change
chrome.storage.onChanged.addListener(async (changes, areaName) => { chrome.storage.onChanged.addListener(async (changes, areaName) => {
@ -24,11 +28,14 @@ import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTa
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])))
}) })
const mountPoint = document.createElement('div') const buttonMountPoint = document.createElement('div')
mountPoint.style.display = 'flex' buttonMountPoint.style.display = 'flex'
function WatchOnLbryButton({ target }: { target?: Target }) { const playerButtonMountPoint = document.createElement('div')
if (!target) return null playerButtonMountPoint.style.display = 'flex'
function WatchOnLbryButton({ source, target }: { source?: Source, target?: Target }) {
if (!target || !source) return null
const url = getLbryUrlByTarget(target) const url = getLbryUrlByTarget(target)
return <div style={{ display: 'flex', justifyContent: 'center', flexDirection: 'column' }}> return <div style={{ display: 'flex', justifyContent: 'center', flexDirection: 'column' }}>
@ -45,104 +52,155 @@ import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTa
border: '0', border: '0',
color: 'whitesmoke', color: 'whitesmoke',
padding: '10px 16px', padding: '10px 16px',
marginRight: target.type === 'channel' ? '10px' : '4px', marginRight: source?.type === 'channel' ? '10px' : '4px',
fontSize: '14px', fontSize: '14px',
textDecoration: 'none', textDecoration: 'none',
...target.platform.button.style?.button, ...target.platform.button.style?.button,
}} }}
onClick={() => findVideoElementAwait().then((videoElement) => { onClick={() => findVideoElementAwait(source).then((videoElement) => {
videoElement.pause() videoElement.pause()
})} })}
> >
<img src={target.platform.button.icon} height={16} <img src={target.platform.button.icon} height={16}
style={{ transform: 'scale(1.5)', ...target.platform.button.style?.icon }} /> style={{ transform: 'scale(1.5)', ...target.platform.button.style?.icon }} />
<span>{target.platform.button.text}</span> <span>{target.type === 'channel' ? 'Channel on' : 'Watch on'} {target.platform.button.platformNameText}</span>
</a> </a>
</div> </div>
} }
function updateButton(target: Target | null): void { function WatchOnLbryPlayerButton({ source, target }: { source?: Source, target?: Target }) {
if (!target) return render(<WatchOnLbryButton />, mountPoint) if (!target || !source) return null
const url = getLbryUrlByTarget(target)
const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname) return <div style={{ display: 'flex', justifyContent: 'center', flexDirection: 'column' }}>
if (!sourcePlatform) return render(<WatchOnLbryButton />, mountPoint) <a href={`${url.href}`} target={target.platform === targetPlatformSettings.app ? '' : '_blank'} role='button'
style={{
const mountBefore = document.querySelector(sourcePlatform.htmlQueries.mountButtonBefore[target.type]) display: 'flex',
if (!mountBefore) return render(<WatchOnLbryButton />, mountPoint) alignItems: 'center',
justifyContent: 'center',
mountBefore.parentElement?.insertBefore(mountPoint, mountBefore) gap: '12px',
render(<WatchOnLbryButton target={target} />, mountPoint) fontWeight: 'bold',
border: '0',
color: 'whitesmoke',
marginRight: '10px',
fontSize: '14px',
textDecoration: 'none',
...target.platform.button.style?.button,
}}
onClick={() => findVideoElementAwait(source).then((videoElement) => {
videoElement.pause()
})}
>
<img src={target.platform.button.icon} height={16}
style={{ transform: 'scale(1.5)', ...target.platform.button.style?.icon }} />
<span>{target.type === 'channel' ? 'Channel on' : 'Watch on'} {target.platform.button.platformNameText}</span>
</a>
</div>
} }
async function findVideoElementAwait() { function updateButton(params: { source: Source, target: Target } | null): void {
const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname) if (!params) {
if (!sourcePlatform) throw new Error(`Unknown source of: ${location.href}`) render(<WatchOnLbryButton />, buttonMountPoint)
render(<WatchOnLbryPlayerButton />, playerButtonMountPoint)
return
}
const mountPlayerButtonBefore = params.source.platform.htmlQueries.mountPoints.mountPlayerButtonBefore ?
document.querySelector(params.source.platform.htmlQueries.mountPoints.mountPlayerButtonBefore) :
null
if (!mountPlayerButtonBefore) render(<WatchOnLbryPlayerButton />, playerButtonMountPoint)
else {
if (mountPlayerButtonBefore.previousSibling !== playerButtonMountPoint)
mountPlayerButtonBefore.parentElement?.insertBefore(playerButtonMountPoint, mountPlayerButtonBefore)
render(<WatchOnLbryPlayerButton target={params.target} source={params.source} />, playerButtonMountPoint)
}
const mountButtonBefore = document.querySelector(params.source.platform.htmlQueries.mountPoints.mountButtonBefore[params.source.type])
if (!mountButtonBefore) render(<WatchOnLbryButton />, playerButtonMountPoint)
else {
if (mountButtonBefore.previousSibling !== buttonMountPoint)
mountButtonBefore.parentElement?.insertBefore(buttonMountPoint, mountButtonBefore)
render(<WatchOnLbryButton target={params.target} source={params.source} />, buttonMountPoint)
}
}
async function findVideoElementAwait(source: Source) {
let videoElement: HTMLVideoElement | null = null let videoElement: HTMLVideoElement | null = null
while (!(videoElement = document.querySelector(sourcePlatform.htmlQueries.videoPlayer))) await sleep(200) while (!(videoElement = document.querySelector(source.platform.htmlQueries.videoPlayer))) await sleep(200)
return videoElement return videoElement
} }
async function getTargetsByURL(...urls: URL[]) { async function getSourceByUrl(url: URL): Promise<Source | null> {
const params: Parameters<typeof requestResolveById>[0] = [] const platform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname)
const platform = targetPlatformSettings[settings.targetPlatform] if (!platform) return null
const datas: Record<string, { url: URL, type: ResolveUrlTypes }> = {} if (url.pathname === '/watch' && url.searchParams.has('v')) {
return {
for (const url of urls) { id: url.searchParams.get('v')!,
if (url.pathname === '/watch' && url.searchParams.has('v')) { platform,
const id = url.searchParams.get('v')! time: url.searchParams.has('t') ? parseYouTubeURLTimeString(url.searchParams.get('t')!) : null,
const type: ResolveUrlTypes = 'video' type: 'video'
params.push({ id, type })
datas[id] = { url, type }
} }
else if (url.pathname.startsWith('/channel/')) { }
const id = url.pathname.substring("/channel/".length) else if (url.pathname.startsWith('/channel/')) {
const type: ResolveUrlTypes = 'channel' return {
params.push({ id, type }) id: url.pathname.substring("/channel/".length),
datas[id] = { url, type } platform,
time: null,
type: 'channel'
} }
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 else if (url.pathname.startsWith('/c/') || url.pathname.startsWith('/user/')) {
// yt front end sucks anyway // We have to download the page content again because these parts of the page are not responsive
const content = await (await fetch(location.href)).text() // yt front end sucks anyway
const prefix = `https://www.youtube.com/feeds/videos.xml?channel_id=` const content = await (await fetch(location.href)).text()
const suffix = `"` const prefix = `https://www.youtube.com/feeds/videos.xml?channel_id=`
const startsAt = content.indexOf(prefix) + prefix.length const suffix = `"`
const endsAt = content.indexOf(suffix, startsAt) const startsAt = content.indexOf(prefix) + prefix.length
const id = content.substring(startsAt, endsAt) const endsAt = content.indexOf(suffix, startsAt)
const type: ResolveUrlTypes = 'channel' const id = content.substring(startsAt, endsAt)
params.push({ id, type }) return {
datas[id] = { url, type } id,
platform,
time: null,
type: 'channel'
} }
} }
const results = Object.entries(await requestResolveById(params)) return null
const targets: Record<string, Target | null> = Object.fromEntries(results.map(([id, result]) => { }
const data = datas[id]
if (!result) return [ async function getTargetsBySources(...sources: Source[]) {
data.url.href, const params: Parameters<typeof requestResolveById>[0] = sources.map((source) => ({ id: source.id, type: source.type }))
null const platform = targetPlatformSettings[settings.targetPlatform]
]
return [ const results = await requestResolveById(params)
data.url.href, const targets: Record<string, Target | null> = Object.fromEntries(
{ sources.map((source) => {
type: data.type, const result = results[source.id]
lbryPathname: result?.id, if (!result) return [
platform, source.id,
time: data.type === 'channel' ? null : (data.url.searchParams.has('t') ? parseYouTubeURLTimeString(data.url.searchParams.get('t')!) : null) null
} ]
]
})) return [
source.id,
{
type: result.type,
lbryPathname: result.id,
platform,
time: source.time
}
]
})
)
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 // 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> { async function requestResolveById(...params: Parameters<typeof resolveById>): ReturnType<typeof resolveById> {
const json = await new Promise<string | null | 'error'>((resolve) => chrome.runtime.sendMessage({ json: JSON.stringify(params) }, resolve)) const json = await new Promise<string | null | 'error'>((resolve) => chrome.runtime.sendMessage({ json: JSON.stringify(params) }, resolve))
if (json === 'error') if (json === 'error') {
{
console.error("Background error on:", params) console.error("Background error on:", params)
throw new Error("Background error.") throw new Error("Background error.")
} }
@ -160,24 +218,28 @@ import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTa
while (true) { while (true) {
await sleep(500) await sleep(500)
const url: URL = (urlCache?.href === location.href) ? urlCache : new URL(location.href); const url: URL = (urlCache?.href === location.href) ? urlCache : new URL(location.href)
const source = await getSourceByUrl(new URL(location.href))
if (!source) continue
try { try {
if (settings.redirect) { if (settings.redirect) {
const target = (await getTargetsByURL(url))[url.href] const target = (await getTargetsBySources(source))[source.id]
if (!target) continue if (!target) continue
if (url === urlCache) continue if (url === urlCache) continue
const lbryURL = getLbryUrlByTarget(target) const lbryURL = getLbryUrlByTarget(target)
findVideoElementAwait().then((videoElement) => { // As soon as video play is ready and start playing, pause it.
findVideoElementAwait(source).then((videoElement) => {
videoElement.addEventListener('play', () => videoElement.pause(), { once: true }) videoElement.addEventListener('play', () => videoElement.pause(), { once: true })
videoElement.pause() 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 }))
// Its not gonna be able to replace anyway // Replace is being used so browser doesnt start an empty window
// This was empty window doesnt stay open // Its not gonna be able to replace anyway, since its a LBRY Uri
location.replace(lbryURL) location.replace(lbryURL)
} }
else { else {
@ -188,30 +250,41 @@ import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTa
} }
else { else {
if (urlCache !== url) updateButton(null) if (urlCache !== url) updateButton(null)
let target = (await getTargetsByURL(url))[url.href] let target = (await getTargetsBySources(source))[source.id]
// There is no target found via API try to check Video Description for LBRY links.
if (!target) { if (!target) {
const descriptionElement = document.querySelector(sourcePlatform.htmlQueries.videoDescription) const linksContainer =
if (descriptionElement) { source.type === 'video' ?
const anchors = Array.from(descriptionElement.querySelectorAll<HTMLAnchorElement>('a')) document.querySelector(source.platform.htmlQueries.videoDescription) :
source.platform.htmlQueries.channelLinks ? document.querySelector(source.platform.htmlQueries.channelLinks) : null
console.log(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 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
if (sourcePlatform === sourcePlatfromSettings['youtube.com']) {
// Extract real link from youtube's redirect link
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
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) {
target = { target = {
lbryPathname: lbryURL.pathname.substring(1), lbryPathname: lbryURL.pathname.substring(1),
time: null, time: null,
type: 'video', type: lbryURL.pathname.substring(1).includes('/') ? 'video' : 'channel',
platform: targetPlatformSettings[settings.targetPlatform] platform: targetPlatformSettings[settings.targetPlatform]
} }
break break
@ -219,14 +292,17 @@ import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTa
} }
} }
} }
if (target?.type === 'video') { if (!target) updateButton(null)
const videoElement = document.querySelector<HTMLVideoElement>(sourcePlatform.htmlQueries.videoPlayer) else {
if (videoElement) target.time = videoElement.currentTime > 3 && videoElement.currentTime < videoElement.duration - 1 ? videoElement.currentTime : null // If target is a video target add timestampt to it
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
}
updateButton({ target, source })
} }
// We run it anyway with null target to hide the button
updateButton(target)
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)

View file

@ -60,7 +60,7 @@ const targetPlatform = (o: {
displayName: string displayName: string
theme: string theme: string
button: { button: {
text: string platformNameText: string,
icon: string icon: string
style?: style?:
{ {
@ -80,7 +80,7 @@ export const targetPlatformSettings = {
displayName: 'Madiator.com', displayName: 'Madiator.com',
theme: 'linear-gradient(130deg, #499375, #43889d)', theme: 'linear-gradient(130deg, #499375, #43889d)',
button: { button: {
text: 'Watch on', platformNameText: '',
icon: chrome.runtime.getURL('assets/icons/lbry/madiator-logo.svg'), icon: chrome.runtime.getURL('assets/icons/lbry/madiator-logo.svg'),
style: { style: {
button: { flexDirection: 'row-reverse' }, button: { flexDirection: 'row-reverse' },
@ -93,7 +93,7 @@ export const targetPlatformSettings = {
displayName: 'Odysee', displayName: 'Odysee',
theme: 'linear-gradient(130deg, #c63d59, #f77937)', theme: 'linear-gradient(130deg, #c63d59, #f77937)',
button: { button: {
text: 'Watch on Odysee', platformNameText: 'Odysee',
icon: chrome.runtime.getURL('assets/icons/lbry/odysee-logo.svg') icon: chrome.runtime.getURL('assets/icons/lbry/odysee-logo.svg')
} }
}), }),
@ -102,7 +102,7 @@ export const targetPlatformSettings = {
displayName: 'LBRY App', displayName: 'LBRY App',
theme: 'linear-gradient(130deg, #499375, #43889d)', theme: 'linear-gradient(130deg, #499375, #43889d)',
button: { button: {
text: 'Watch on LBRY', platformNameText: 'LBRY',
icon: chrome.runtime.getURL('assets/icons/lbry/lbry-logo.svg') icon: chrome.runtime.getURL('assets/icons/lbry/lbry-logo.svg')
} }
}), }),
@ -113,9 +113,13 @@ export const targetPlatformSettings = {
const sourcePlatform = (o: { const sourcePlatform = (o: {
hostnames: string[] hostnames: string[]
htmlQueries: { htmlQueries: {
mountButtonBefore: Record<ResolveUrlTypes, string>, mountPoints: {
mountButtonBefore: Record<ResolveUrlTypes, string>,
mountPlayerButtonBefore: string | null,
}
videoPlayer: string, videoPlayer: string,
videoDescription: string videoDescription: string
channelLinks: string | null
} }
}) => o }) => o
export type SourcePlatform = ReturnType<typeof sourcePlatform> export type SourcePlatform = ReturnType<typeof sourcePlatform>
@ -130,24 +134,32 @@ export const sourcePlatfromSettings = {
"youtube.com": sourcePlatform({ "youtube.com": sourcePlatform({
hostnames: ['www.youtube.com'], hostnames: ['www.youtube.com'],
htmlQueries: { htmlQueries: {
mountButtonBefore: { mountPoints: {
video: 'ytd-video-owner-renderer~#subscribe-button', mountButtonBefore: {
channel: '#channel-header-container #buttons' video: 'ytd-video-owner-renderer~#subscribe-button',
channel: '#channel-header-container #buttons'
},
mountPlayerButtonBefore: 'ytd-player .ytp-right-controls',
}, },
videoPlayer: '#ytd-player video', videoPlayer: '#ytd-player video',
videoDescription: 'ytd-video-secondary-info-renderer #description' videoDescription: 'ytd-video-secondary-info-renderer #description',
channelLinks: '#channel-header #links-holder'
} }
}), }),
"yewtu.be": sourcePlatform({ "yewtu.be": sourcePlatform({
hostnames: ['yewtu.be', 'vid.puffyan.us', 'invidio.xamh.de', 'invidious.kavin.rocks'], hostnames: ['yewtu.be', 'vid.puffyan.us', 'invidio.xamh.de', 'invidious.kavin.rocks'],
htmlQueries: { htmlQueries: {
mountButtonBefore: mountPoints: {
{ mountButtonBefore:
video: '#watch-on-youtube', {
channel: '#subscribe' video: '#watch-on-youtube',
channel: '#subscribe'
},
mountPlayerButtonBefore: null,
}, },
videoPlayer: '#player-container video', videoPlayer: '#player-container video',
videoDescription: '#descriptionWrapper' videoDescription: '#descriptionWrapper',
channelLinks: null
} }
}) })
} }