mirror of
https://github.com/LBRYFoundation/Watch-on-LBRY.git
synced 2025-08-23 17:47:26 +00:00
commit
63a251c0ae
15 changed files with 167 additions and 80 deletions
5
global.d.ts
vendored
5
global.d.ts
vendored
|
@ -1,4 +1,9 @@
|
|||
declare module '*.md' {
|
||||
var _: string
|
||||
export default _
|
||||
}
|
||||
|
||||
declare namespace chrome
|
||||
{
|
||||
export const action = chrome.browserAction
|
||||
}
|
56
manifest.v2.json
Normal file
56
manifest.v2.json
Normal file
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Watch on LBRY",
|
||||
"version": "2.0.0",
|
||||
"icons": {
|
||||
"16": "assets/icons/wol/icon16.png",
|
||||
"48": "assets/icons/wol/icon48.png",
|
||||
"128": "assets/icons/wol/icon128.png"
|
||||
},
|
||||
"permissions": [
|
||||
"https://www.youtube.com/",
|
||||
"https://yewtu.be/",
|
||||
"https://vid.puffyan.us/",
|
||||
"https://invidio.xamh.de/",
|
||||
"https://invidious.kavin.rocks/",
|
||||
"https://api.odysee.com/",
|
||||
"https://lbry.tv/",
|
||||
"https://odysee.com/",
|
||||
"https://madiator.com/",
|
||||
"https://finder.madiator.com/",
|
||||
"tabs",
|
||||
"storage"
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
"pages/popup/index.html",
|
||||
"pages/YTtoLBRY/index.html",
|
||||
"pages/import/index.html",
|
||||
"assets/icons/lbry/lbry-logo.svg",
|
||||
"assets/icons/lbry/odysee-logo.svg",
|
||||
"assets/icons/lbry/madiator-logo.svg"
|
||||
],
|
||||
"browser_action": {
|
||||
"default_title": "Watch on LBRY",
|
||||
"default_popup": "pages/popup/index.html"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"https://www.youtube.com/*",
|
||||
"https://yewtu.be/*",
|
||||
"https://vid.puffyan.us/*",
|
||||
"https://invidio.xamh.de/*",
|
||||
"https://invidious.kavin.rocks/*"
|
||||
],
|
||||
"js": [
|
||||
"scripts/ytContent.js"
|
||||
]
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"scripts": [
|
||||
"service-worker-entry-point.js"
|
||||
],
|
||||
"persistent": true
|
||||
}
|
||||
}
|
|
@ -1,7 +1,17 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Watch on LBRY",
|
||||
"version": "2.0.0",
|
||||
"icons": {
|
||||
"16": "assets/icons/wol/icon16.png",
|
||||
"48": "assets/icons/wol/icon48.png",
|
||||
"128": "assets/icons/wol/icon128.png"
|
||||
},
|
||||
"permissions": [
|
||||
"tabs",
|
||||
"storage"
|
||||
],
|
||||
"host_permissions": [
|
||||
"https://www.youtube.com/",
|
||||
"https://yewtu.be/",
|
||||
"https://vid.puffyan.us/",
|
||||
|
@ -11,10 +21,24 @@
|
|||
"https://lbry.tv/",
|
||||
"https://odysee.com/",
|
||||
"https://madiator.com/",
|
||||
"https://finder.madiator.com/",
|
||||
"tabs",
|
||||
"storage"
|
||||
"https://finder.madiator.com/"
|
||||
],
|
||||
"web_accessible_resources": [{
|
||||
"resources": [
|
||||
"pages/popup/index.html",
|
||||
"pages/YTtoLBRY/index.html",
|
||||
"pages/import/index.html",
|
||||
"assets/icons/lbry/lbry-logo.svg",
|
||||
"assets/icons/lbry/odysee-logo.svg",
|
||||
"assets/icons/lbry/madiator-logo.svg"
|
||||
],
|
||||
"matches": ["<all_urls>"],
|
||||
"extension_ids": []
|
||||
}],
|
||||
"action": {
|
||||
"default_title": "Watch on LBRY",
|
||||
"default_popup": "pages/popup/index.html"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
|
@ -30,28 +54,6 @@
|
|||
}
|
||||
],
|
||||
"background": {
|
||||
"scripts": [
|
||||
"settings/background.js",
|
||||
"scripts/background.js"
|
||||
],
|
||||
"persistent": true
|
||||
},
|
||||
"browser_action": {
|
||||
"default_title": "Watch on LBRY",
|
||||
"default_popup": "pages/popup/index.html"
|
||||
},
|
||||
"web_accessible_resources": [
|
||||
"pages/popup/index.html",
|
||||
"pages/YTtoLBRY/index.html",
|
||||
"pages/import/index.html",
|
||||
"assets/icons/lbry/lbry-logo.svg",
|
||||
"assets/icons/lbry/odysee-logo.svg",
|
||||
"assets/icons/lbry/madiator-logo.svg"
|
||||
],
|
||||
"icons": {
|
||||
"16": "assets/icons/wol/icon16.png",
|
||||
"48": "assets/icons/wol/icon48.png",
|
||||
"128": "assets/icons/wol/icon128.png"
|
||||
},
|
||||
"manifest_version": 2
|
||||
"service_worker": "service-worker-entry-point.js"
|
||||
}
|
||||
}
|
22
package.json
22
package.json
|
@ -5,11 +5,29 @@
|
|||
"scripts": {
|
||||
"build:assets": "cpx \"src/**/*.{json,png,svg}\" dist/",
|
||||
"watch:assets": "cpx --watch -v \"src/**/*.{json,png,svg}\" dist/",
|
||||
|
||||
"build:parcel": "cross-env NODE_ENV=production parcel build --no-source-maps --no-minify \"src/**/*.{js,jsx,ts,tsx}\" \"src/**/*.html\" \"src/**/*.md\"",
|
||||
"watch:parcel": "parcel watch --no-hmr --no-source-maps \"src/**/*.{js,jsx,ts,tsx}\" \"src/**/*.html\" \"src/**/*.md\"",
|
||||
|
||||
"build:webext": "web-ext build --source-dir ./dist --overwrite-dest",
|
||||
"build": "npm-run-all -l -p build:parcel build:assets",
|
||||
"watch": "npm-run-all -l -p watch:parcel watch:assets",
|
||||
|
||||
"clear:dist": "rm -r ./dist ; mkdir ./dist",
|
||||
|
||||
"build:base": "npm-run-all -l -p build:parcel build:assets",
|
||||
|
||||
"pick:manifest:v2": "cp -b ./manifest.v2.json ./dist/manifest.json",
|
||||
"pick:manifest:v3": "cp -b ./manifest.v3.json ./dist/manifest.json",
|
||||
|
||||
"build:v2": "npm run clear:dist ; npm run build:base && npm run pick:manifest:v2",
|
||||
"build:v3": "npm run clear:dist ; npm run build:base && npm run pick:manifest:v3",
|
||||
|
||||
"build": "npm run build:v3",
|
||||
|
||||
"watch:v2": "npm run clear:dist ; npm run pick:manifest:v2 && npm-run-all -l -p watch:parcel watch:assets",
|
||||
"watch:v3": "npm run clear:dist ; npm run pick:manifest:v3 && npm-run-all -l -p watch:parcel watch:assets",
|
||||
|
||||
"watch": "npm run watch:v3",
|
||||
|
||||
"start:chrome": "web-ext run -t chromium --source-dir ./dist",
|
||||
"start:firefox": "web-ext run --source-dir ./dist",
|
||||
"test": "jest"
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import path from 'path'
|
||||
import { DialogManager } from '../../components/dialogs'
|
||||
import type { DialogManager } from '../../components/dialogs'
|
||||
import { getExtensionSettingsAsync, setExtensionSetting, ytUrlResolversSettings } from "../../settings"
|
||||
import { getFileContent } from '../file'
|
||||
|
||||
async function generateKeys() {
|
||||
const keys = await window.crypto.subtle.generateKey(
|
||||
const keys = await crypto.subtle.generateKey(
|
||||
{
|
||||
name: "RSASSA-PKCS1-v1_5",
|
||||
// Consider using a 4096-bit key for systems that require long-term security
|
||||
|
@ -23,7 +23,7 @@ async function generateKeys() {
|
|||
}
|
||||
|
||||
async function exportPrivateKey(key: CryptoKey) {
|
||||
const exported = await window.crypto.subtle.exportKey(
|
||||
const exported = await crypto.subtle.exportKey(
|
||||
"pkcs8",
|
||||
key
|
||||
)
|
||||
|
@ -34,7 +34,7 @@ const publicKeyPrefix = `MEwwDQYJKoZIhvcNAQEBBQADOwAwOAIxA`
|
|||
const publicKeySuffix = `IDAQAB` //`wIDAQAB` `WIDAQAB`
|
||||
const publicKeyLength = 65
|
||||
async function exportPublicKey(key: CryptoKey) {
|
||||
const exported = await window.crypto.subtle.exportKey(
|
||||
const exported = await crypto.subtle.exportKey(
|
||||
"spki",
|
||||
key
|
||||
)
|
||||
|
@ -43,7 +43,7 @@ async function exportPublicKey(key: CryptoKey) {
|
|||
}
|
||||
|
||||
function importPrivateKey(base64: string) {
|
||||
return window.crypto.subtle.importKey(
|
||||
return crypto.subtle.importKey(
|
||||
"pkcs8",
|
||||
Buffer.from(base64, 'base64'),
|
||||
{
|
||||
|
@ -56,7 +56,7 @@ function importPrivateKey(base64: string) {
|
|||
}
|
||||
|
||||
export async function sign(data: string, privateKey: string) {
|
||||
return Buffer.from(await window.crypto.subtle.sign(
|
||||
return Buffer.from(await crypto.subtle.sign(
|
||||
{ name: "RSASSA-PKCS1-v1_5" },
|
||||
await importPrivateKey(privateKey),
|
||||
await crypto.subtle.digest({ name: 'SHA-1' }, Buffer.from(data))
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
interface YtExportedJsonSubscription {
|
||||
id: string
|
||||
etag: string
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
let db: IDBDatabase | null = null
|
||||
|
||||
if (typeof chrome.extension === 'undefined') throw new Error("YT urlCache can only be accessed from extension windows and service-workers.")
|
||||
|
||||
if (typeof self.indexedDB !== 'undefined') {
|
||||
const openRequest = indexedDB.open("yt-url-resolver-cache")
|
||||
openRequest.addEventListener('upgradeneeded', () => openRequest.result.createObjectStore("store").createIndex("expireAt", "expireAt"))
|
||||
|
@ -81,5 +83,5 @@ async function get(id: string): Promise<string | null | undefined> {
|
|||
return response.value
|
||||
}
|
||||
|
||||
export const LbryPathnameCache = { put, get, clearAll }
|
||||
export const lbryUrlCache = { put, get, clearAll }
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { chunk } from "lodash"
|
|||
import path from "path"
|
||||
import { getExtensionSettingsAsync, ytUrlResolversSettings } from "../../settings"
|
||||
import { sign } from "../crypto"
|
||||
import { LbryPathnameCache } from "./urlCache"
|
||||
import { lbryUrlCache } from "./urlCache"
|
||||
|
||||
const QUERY_CHUNK_SIZE = 100
|
||||
|
||||
|
@ -27,7 +27,7 @@ export async function resolveById(params: Paramaters, progressCallback?: (progre
|
|||
// Check for cache first, add them to the results if there are any cache
|
||||
// And remove them from the params, so we dont request for them
|
||||
params = (await Promise.all(params.map(async (item) => {
|
||||
const cachedLbryUrl = await LbryPathnameCache.get(item.id)
|
||||
const cachedLbryUrl = await lbryUrlCache.get(item.id)
|
||||
|
||||
// Cache can be null, if there is no lbry url yet
|
||||
if (cachedLbryUrl !== undefined) {
|
||||
|
@ -58,7 +58,7 @@ export async function resolveById(params: Paramaters, progressCallback?: (progre
|
|||
for (const item of params) {
|
||||
const lbryUrl = (item.type === 'channel' ? response.data.channels : response.data.videos)?.[item.id]?.replaceAll('#', ':') ?? null
|
||||
// we cache it no matter if its null or not
|
||||
await LbryPathnameCache.put(lbryUrl, item.id)
|
||||
await lbryUrlCache.put(lbryUrl, item.id)
|
||||
|
||||
if (lbryUrl) results[item.id] = { id: lbryUrl, type: item.type }
|
||||
}
|
||||
|
|
|
@ -3,6 +3,21 @@ import { useState } from 'preact/hooks'
|
|||
import { createDialogManager, Dialogs } from '../../components/dialogs'
|
||||
import { importProfileKeysFromFile, inputKeyFile } from '../../modules/crypto'
|
||||
|
||||
export async function openImportPopup() {
|
||||
const importPopupWindow = open(
|
||||
'/pages/import/index.html',
|
||||
'Import Profile',
|
||||
[
|
||||
`height=${Math.max(document.body.clientHeight, screen.height * .5)}`,
|
||||
`width=${document.body.clientWidth}`,
|
||||
`toolbar=0,menubar=0,location=0`,
|
||||
`top=${screenY}`,
|
||||
`left=${screenX}`
|
||||
].join(','))
|
||||
close()
|
||||
importPopupWindow?.focus()
|
||||
}
|
||||
|
||||
function ImportPage() {
|
||||
const [loading, updateLoading] = useState(() => false)
|
||||
|
||||
|
|
|
@ -2,8 +2,9 @@ import { h, render } from 'preact'
|
|||
import { useState } from 'preact/hooks'
|
||||
import { createDialogManager, Dialogs } from '../../components/dialogs'
|
||||
import { exportProfileKeysAsFile, friendlyPublicKey, generateProfileAndSetNickname, getProfile, purgeProfile, resetProfileSettings } from '../../modules/crypto'
|
||||
import { LbryPathnameCache } from '../../modules/yt/urlCache'
|
||||
import { lbryUrlCache } from '../../modules/yt/urlCache'
|
||||
import { getTargetPlatfromSettingsEntiries, getYtUrlResolversSettingsEntiries, setExtensionSetting, useExtensionSettings } from '../../settings'
|
||||
import { openImportPopup } from '../import/main'
|
||||
|
||||
|
||||
/** Gets all the options for redirect destinations as selection options */
|
||||
|
@ -31,21 +32,6 @@ function WatchOnLbryPopup(params: { profile: Awaited<ReturnType<typeof getProfil
|
|||
}
|
||||
}
|
||||
|
||||
async function importButtonClick() {
|
||||
const importPopupWindow = open(
|
||||
'/pages/import/index.html',
|
||||
'Import Profile',
|
||||
[
|
||||
`height=${Math.max(document.body.clientHeight, screen.height * .5)}`,
|
||||
`width=${document.body.clientWidth}`,
|
||||
`toolbar=0,menubar=0,location=0`,
|
||||
`top=${screenY}`,
|
||||
`left=${screenX}`
|
||||
].join(','))
|
||||
close()
|
||||
importPopupWindow?.focus()
|
||||
}
|
||||
|
||||
return <div id='popup'>
|
||||
<Dialogs manager={dialogManager} />
|
||||
{
|
||||
|
@ -100,7 +86,7 @@ function WatchOnLbryPopup(params: { profile: Awaited<ReturnType<typeof getProfil
|
|||
<a onClick={() => exportProfileKeysAsFile()} className={`button active`}>
|
||||
Export
|
||||
</a>
|
||||
<a onClick={() => importButtonClick()}
|
||||
<a onClick={() => openImportPopup()}
|
||||
className={`button`}
|
||||
>
|
||||
Import
|
||||
|
@ -139,7 +125,7 @@ function WatchOnLbryPopup(params: { profile: Awaited<ReturnType<typeof getProfil
|
|||
<label>You don't have a profile.</label>
|
||||
<p>You can either import keypair for an existing profile or generate a new profile keypair.</p>
|
||||
<div className='options'>
|
||||
<a onClick={() => importButtonClick()} className={`button`}>
|
||||
<a onClick={() => openImportPopup()} className={`button`}>
|
||||
Import
|
||||
</a>
|
||||
<a onClick={() => loads(generateProfileAndSetNickname(dialogManager)).then(() => renderPopup())} className={`button active`}>
|
||||
|
@ -180,7 +166,7 @@ function WatchOnLbryPopup(params: { profile: Awaited<ReturnType<typeof getProfil
|
|||
</a>
|
||||
)}
|
||||
</div>
|
||||
<a onClick={() => loads(LbryPathnameCache.clearAll().then(() => dialogManager.alert("Cleared Cache!")))} className={`button active`}>
|
||||
<a onClick={() => loads(lbryUrlCache.clearAll().then(() => dialogManager.alert("Cleared Cache!")))} className={`button active`}>
|
||||
Clear Resolver Cache
|
||||
</a>
|
||||
</section>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { h, render } from 'preact'
|
||||
import { parseYouTubeURLTimeString } from '../modules/yt'
|
||||
import { resolveById } from '../modules/yt/urlResolve'
|
||||
import type { resolveById } from '../modules/yt/urlResolve'
|
||||
import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, TargetPlatform, targetPlatformSettings } from '../settings'
|
||||
|
||||
const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t))
|
||||
|
|
2
src/service-worker-entry-point.ts
Normal file
2
src/service-worker-entry-point.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
import './settings/background'
|
||||
import './scripts/background'
|
|
@ -1,31 +1,33 @@
|
|||
import { DEFAULT_SETTINGS, ExtensionSettings, getExtensionSettingsAsync, setExtensionSetting, targetPlatformSettings, ytUrlResolversSettings } from '.'
|
||||
import { DEFAULT_SETTINGS, ExtensionSettings, getExtensionSettingsAsync, setExtensionSetting, targetPlatformSettings, ytUrlResolversSettings } from '../settings'
|
||||
|
||||
// This is for manifest v2 and v3
|
||||
const chromeAction = chrome.action ?? chrome.browserAction
|
||||
|
||||
/** Reset settings to default value and update the browser badge text */
|
||||
async function initSettings() {
|
||||
let settings = await getExtensionSettingsAsync()
|
||||
let settings = await getExtensionSettingsAsync()
|
||||
|
||||
// get all the values that aren't set and use them as a change set
|
||||
const invalidEntries = (Object.entries(DEFAULT_SETTINGS) as Array<[keyof ExtensionSettings, ExtensionSettings[keyof ExtensionSettings]]>)
|
||||
.filter(([k]) => settings[k] === undefined || settings[k] === null)
|
||||
// get all the values that aren't set and use them as a change set
|
||||
const invalidEntries = (Object.entries(DEFAULT_SETTINGS) as Array<[keyof ExtensionSettings, ExtensionSettings[keyof ExtensionSettings]]>)
|
||||
.filter(([k]) => settings[k] === undefined || settings[k] === null)
|
||||
|
||||
// fix our local var and set it in storage for later
|
||||
if (invalidEntries.length > 0) {
|
||||
const changeSet = Object.fromEntries(invalidEntries)
|
||||
chrome.storage.local.set(changeSet)
|
||||
settings = await getExtensionSettingsAsync()
|
||||
}
|
||||
// fix our local var and set it in storage for later
|
||||
if (invalidEntries.length > 0) {
|
||||
const changeSet = Object.fromEntries(invalidEntries)
|
||||
chrome.storage.local.set(changeSet)
|
||||
settings = await getExtensionSettingsAsync()
|
||||
}
|
||||
|
||||
if (!Object.keys(targetPlatformSettings).includes(settings.targetPlatform)) setExtensionSetting('targetPlatform', DEFAULT_SETTINGS.targetPlatform)
|
||||
if (!Object.keys(ytUrlResolversSettings).includes(settings.urlResolver)) setExtensionSetting('urlResolver', DEFAULT_SETTINGS.urlResolver)
|
||||
if (!Object.keys(targetPlatformSettings).includes(settings.targetPlatform)) setExtensionSetting('targetPlatform', DEFAULT_SETTINGS.targetPlatform)
|
||||
if (!Object.keys(ytUrlResolversSettings).includes(settings.urlResolver)) setExtensionSetting('urlResolver', DEFAULT_SETTINGS.urlResolver)
|
||||
|
||||
chrome.browserAction.setBadgeText({ text: settings.redirect ? 'ON' : 'OFF' })
|
||||
chromeAction.setBadgeText({ text: settings.redirect ? 'ON' : 'OFF' })
|
||||
}
|
||||
|
||||
chrome.storage.onChanged.addListener((changes, areaName) => {
|
||||
if (areaName !== 'local' || !changes.redirect) return
|
||||
chrome.browserAction.setBadgeText({ text: changes.redirect.newValue ? 'ON' : 'OFF' })
|
||||
if (areaName !== 'local' || !changes.redirect) return
|
||||
chromeAction.setBadgeText({ text: changes.redirect.newValue ? 'ON' : 'OFF' })
|
||||
})
|
||||
|
||||
|
||||
chrome.runtime.onStartup.addListener(initSettings)
|
||||
chrome.runtime.onInstalled.addListener(initSettings)
|
||||
chrome.runtime.onInstalled.addListener(initSettings)
|
|
@ -1,4 +1,4 @@
|
|||
import { JSX } from "preact"
|
||||
import type { JSX } from "preact"
|
||||
import { useEffect, useReducer } from "preact/hooks"
|
||||
|
||||
export interface ExtensionSettings {
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
"importsNotUsedAsValues": "error", /* Import types always with `import type` */
|
||||
|
||||
/* Module Resolution Options */
|
||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
|
@ -53,7 +54,6 @@
|
|||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
|
|
Loading…
Add table
Reference in a new issue