diff --git a/docs/DOCKER.md b/docs/DOCKER.md index ad685d1c..94daa4ac 100644 --- a/docs/DOCKER.md +++ b/docs/DOCKER.md @@ -59,14 +59,14 @@ server { ``` ### Nginx Proxy Manager example 1. Tab: Details - Create a new proxy host with SSL on (Let's Encrypt or your own certificate). -2. Tab: Custom locations, fill in IP and port. Click gear icon to add some security headers. +2. Tab: Custom locations, fill in IP and port. Click gear icon to add some security headers. -Click image for fullsize: +Click image for fullsize: -**Add:** +**Add:** ```nginx if ($request_method = OPTIONS) { return 204; @@ -97,12 +97,37 @@ http: - PATCH - PUT - DELETE - accessControlAllowHeaders: + accessControlAllowHeaders: - User-Agent - - Authorization + - Authorization - Content-Type ``` +### Invidious Companion support +The `VITE_DEFAULT_COMPANION_INSTANCE` environment variable allows you to specify a custom [Invidious Companion](https://github.com/iv-org/invidious-companion) instance. + +To use this with Materialious, your Invidious Companion instance must be accessible with proper CORS headers. Fortunately, you can reuse the same reverse proxy configuration (with CORS modifications) that you applied to your Invidious instance—just apply it to your companion domain as well. + +For example, if you're using Caddy, you can configure your companion domain like this: + +```caddy +companion.example.com { + @cors_preflight { + method OPTIONS + } + respond @cors_preflight 204 + + header Access-Control-Allow-Credentials true + header Access-Control-Allow-Origin "https://materialious.example.com" { + defer + } + header Access-Control-Allow-Methods "GET, POST, OPTIONS, HEAD, PATCH, PUT, DELETE" + header Access-Control-Allow-Headers "User-Agent, Authorization, Content-Type" + + reverse_proxy localhost:3000 +} +``` + ### Other Please open a PR request or issue if you implement this in a different reverse proxy. @@ -132,10 +157,13 @@ services: # URL to your proxied Invidious instance VITE_DEFAULT_INVIDIOUS_INSTANCE: "https://invidious.materialio.us" + # URL to your proxied Companion instance + VITE_DEFAULT_COMPANION_INSTANCE: "https://companion.materialio.us" + # URL TO RYD # Leave blank to disable completely. VITE_DEFAULT_RETURNYTDISLIKES_INSTANCE: "https://returnyoutubedislikeapi.com" - + # URL to Sponsorblock # Leave blank to completely disable sponsorblock. VITE_DEFAULT_SPONSERBLOCK_INSTANCE: "https://sponsor.ajay.app" @@ -148,9 +176,6 @@ services: # Look at "Overwriting Materialious defaults" for all the accepted values. VITE_DEFAULT_SETTINGS: '{"themeColor": "#2596be","region": "US"}' - - # Set the default video/audio bitrate to use, -1 automatic, 999999 always best quality - VITE_DEFAULT_DASH_BITRATE: -1 ``` ### Overwriting Materialious defaults @@ -270,4 +295,4 @@ Add these additional environment variables to Materialious. VITE_DEFAULT_PEERJS_HOST: "peerjs.example.com" VITE_DEFAULT_PEERJS_PATH: "/" VITE_DEFAULT_PEERJS_PORT: 443 -``` \ No newline at end of file +``` diff --git a/materialious/Dockerfile b/materialious/Dockerfile index 806ea6d9..d61d1192 100644 --- a/materialious/Dockerfile +++ b/materialious/Dockerfile @@ -11,6 +11,7 @@ COPY . . # Create placeholder env vars RUN echo "VITE_DEFAULT_INVIDIOUS_INSTANCE=VITE_DEFAULT_INVIDIOUS_INSTANCE_PLACEHOLDER" > .env && \ + echo "VITE_DEFAULT_COMPANION_INSTANCE=VITE_DEFAULT_COMPANION_INSTANCE_PLACEHOLDER" >> .env && \ echo "VITE_DEFAULT_SPONSERBLOCK_INSTANCE=VITE_DEFAULT_SPONSERBLOCK_INSTANCE_PLACEHOLDER" >> .env && \ echo "VITE_DEFAULT_DEARROW_INSTANCE=VITE_DEFAULT_DEARROW_INSTANCE_PLACEHOLDER" >> .env && \ echo "VITE_DEFAULT_DEARROW_THUMBNAIL_INSTANCE=VITE_DEFAULT_DEARROW_THUMBNAIL_INSTANCE_PLACEHOLDER" >> .env && \ diff --git a/materialious/android/app/build.gradle b/materialious/android/app/build.gradle index ceab7ec6..bd3d5665 100644 --- a/materialious/android/app/build.gradle +++ b/materialious/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "us.materialio.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 116 - versionName "1.8.12" + versionCode 117 + versionName "1.9.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/materialious/electron/materialious.metainfo.xml b/materialious/electron/materialious.metainfo.xml index 03440bbe..95cc5cdd 100644 --- a/materialious/electron/materialious.metainfo.xml +++ b/materialious/electron/materialious.metainfo.xml @@ -61,7 +61,11 @@ - + + + https://github.com/Materialious/Materialious/releases/tag/1.9.0 + + https://github.com/Materialious/Materialious/releases/tag/1.8.12 diff --git a/materialious/electron/package-lock.json b/materialious/electron/package-lock.json index 1081eca8..6564b0ba 100644 --- a/materialious/electron/package-lock.json +++ b/materialious/electron/package-lock.json @@ -1,17 +1,17 @@ { "name": "Materialious", - "version": "1.8.10", + "version": "1.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "Materialious", - "version": "1.8.10", + "version": "1.9.0", "license": "MIT", "dependencies": { "@capacitor-community/electron": "^5.0.0", "bgutils-js": "^3.2.0", - "capacitor-nodejs": "https://github.com/EdenwareApps/Capacitor-NodeJS/releases/download/v1.0.0-beta.7/capacitor6-nodejs.tgz", + "capacitor-nodejs": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.8/capacitor-nodejs.tgz", "chokidar": "~4.0.3", "electron-is-dev": "~2.0.0", "electron-serve": "~1.1.0", @@ -1799,10 +1799,13 @@ } }, "node_modules/capacitor-nodejs": { - "version": "1.0.0-beta.7", - "resolved": "https://github.com/EdenwareApps/Capacitor-NodeJS/releases/download/v1.0.0-beta.7/capacitor6-nodejs.tgz", - "integrity": "sha512-7NwSMxE06nqpuFKyQlt3mWo+VG6Od0d6PBxPKVyPmaptE6sSoimzU/uzQXPWoHV5bwbjgcF+R0BSJAZAyy4gsg==", + "version": "1.0.0-beta.8", + "resolved": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.8/capacitor-nodejs.tgz", + "integrity": "sha512-VAMCTBqz+sA3762kFhSyzXS0unm0EpsVSHLmDYsTYPTiXXZaFcGk27vfXakRyyXJexAUG2Y5rZka5BvH6EmeZw==", "license": "MIT", + "engines": { + "node": ">=20.10" + }, "peerDependencies": { "@capacitor/core": "^6.0.0" } diff --git a/materialious/electron/package.json b/materialious/electron/package.json index 30da3ba0..f9990e30 100644 --- a/materialious/electron/package.json +++ b/materialious/electron/package.json @@ -1,6 +1,6 @@ { "name": "Materialious", - "version": "1.8.12", + "version": "1.9.0", "description": "Modern material design for Invidious.", "author": { "name": "Ward Pearce", @@ -24,7 +24,7 @@ "dependencies": { "@capacitor-community/electron": "^5.0.0", "bgutils-js": "^3.2.0", - "capacitor-nodejs": "https://github.com/EdenwareApps/Capacitor-NodeJS/releases/download/v1.0.0-beta.7/capacitor6-nodejs.tgz", + "capacitor-nodejs": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.8/capacitor-nodejs.tgz", "chokidar": "~4.0.3", "electron-is-dev": "~2.0.0", "electron-serve": "~1.1.0", @@ -44,4 +44,4 @@ "capacitor", "electron" ] -} \ No newline at end of file +} diff --git a/materialious/electron/src/index.ts b/materialious/electron/src/index.ts index 92b385cb..476b8cd3 100644 --- a/materialious/electron/src/index.ts +++ b/materialious/electron/src/index.ts @@ -4,19 +4,13 @@ import { setupElectronDeepLinking } from '@capacitor-community/electron'; import type { MenuItemConstructorOptions } from 'electron'; -import { app, ipcMain, MenuItem } from 'electron'; +import { app, ipcMain, MenuItem, session, webContents } from 'electron'; import electronIsDev from 'electron-is-dev'; import unhandled from 'electron-unhandled'; import { autoUpdater } from 'electron-updater'; import { JSDOM } from 'jsdom'; -import BG, { - buildURL, - DescrambledChallenge, - GOOG_API_KEY, - USER_AGENT, - WebPoSignalOutput -} from 'bgutils-js'; +import BG, { buildURL, GOOG_API_KEY, USER_AGENT, WebPoSignalOutput } from 'bgutils-js'; import { ElectronCapacitorApp, setupContentSecurityPolicy, setupReloadWatcher } from './setup'; // Graceful handling of unhandled errors. @@ -54,6 +48,8 @@ if (electronIsDev) { setupReloadWatcher(myCapacitorApp); } +let allowInsecureSSL = false; + // Run Application (async () => { // Wait for electron app to be ready. @@ -66,6 +62,15 @@ if (electronIsDev) { autoUpdater.checkForUpdatesAndNotify(); })(); +app.on('certificate-error', (event, webContents, url, error, certificate, callback) => { + if (allowInsecureSSL) { + event.preventDefault(); // prevent default behavior (blocking the cert) + callback(true); // trust the certificate + } else { + callback(false); // reject others + } +}); + // Handle when all of our windows are close (platforms have their own expectations). app.on('window-all-closed', function () { // On OS X it is common for applications and their menu bar @@ -154,3 +159,11 @@ ipcMain.handle( return await integrityTokenBasedMinter.mintAsWebsafeString(decodeURIComponent(visitorData)); } ); + +ipcMain.handle('setAllowInsecureSSL', async (_, allow) => { + allowInsecureSSL = allow; + await session.defaultSession.clearAuthCache(); + await session.defaultSession.clearCache(); + + return allowInsecureSSL; +}); diff --git a/materialious/electron/src/preload.ts b/materialious/electron/src/preload.ts index e54e2e00..278f9285 100644 --- a/materialious/electron/src/preload.ts +++ b/materialious/electron/src/preload.ts @@ -5,5 +5,8 @@ require('./rt/electron-rt'); contextBridge.exposeInMainWorld('electronAPI', { generatePoToken: (bgChallenge: DescrambledChallenge, requestKey: string, visitorData: string) => - ipcRenderer.invoke('generatePoToken', bgChallenge, requestKey, visitorData) + ipcRenderer.invoke('generatePoToken', bgChallenge, requestKey, visitorData), + setAllowInsecureSSL: async (allow: boolean) => { + return await ipcRenderer.invoke('setAllowInsecureSSL', allow); + } }); diff --git a/materialious/package.json b/materialious/package.json index e72ad0d0..ecaf8c36 100644 --- a/materialious/package.json +++ b/materialious/package.json @@ -1,6 +1,6 @@ { "name": "materialious", - "version": "1.8.12", + "version": "1.9.0", "private": true, "scripts": { "dev": "vite dev", diff --git a/materialious/replace_env_vars.sh b/materialious/replace_env_vars.sh index 329fb6d3..18f71f88 100644 --- a/materialious/replace_env_vars.sh +++ b/materialious/replace_env_vars.sh @@ -3,6 +3,7 @@ ROOT_DIR=/usr/share/nginx/html # Replace env vars in JavaScript and HTML files served by NGINX find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_INVIDIOUS_INSTANCE_PLACEHOLDER|'"$VITE_DEFAULT_INVIDIOUS_INSTANCE"'|g' {} + +find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_COMPANION_INSTANCE_PLACEHOLDER|'"$VITE_DEFAULT_COMPANION_INSTANCE"'|g' {} + find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_RETURNYTDISLIKES_INSTANCE_PLACEHOLDER|'"$VITE_DEFAULT_RETURNYTDISLIKES_INSTANCE"'|g' {} + find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_SPONSERBLOCK_INSTANCE_PLACEHOLDER|'"$VITE_DEFAULT_SPONSERBLOCK_INSTANCE"'|g' {} + find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_DEARROW_THUMBNAIL_INSTANCE_PLACEHOLDER|'"$VITE_DEFAULT_DEARROW_THUMBNAIL_INSTANCE"'|g' {} + diff --git a/materialious/src/app.d.ts b/materialious/src/app.d.ts index 0ce56bce..753ee6b1 100644 --- a/materialious/src/app.d.ts +++ b/materialious/src/app.d.ts @@ -18,6 +18,7 @@ declare global { requestKey: string, visitorData: string ) => Promise; + setAllowInsecureSSL: (allowInsecureSSL: boolean) => Promoise; }; } } diff --git a/materialious/src/lib/android/http/androidRequests.ts b/materialious/src/lib/android/http/androidRequests.ts index 50676c9c..a3fe52ed 100644 --- a/materialious/src/lib/android/http/androidRequests.ts +++ b/materialious/src/lib/android/http/androidRequests.ts @@ -1,5 +1,4 @@ import { goto } from '$app/navigation'; -import { interfaceAllowInsecureRequests } from '$lib/store'; import { Capacitor } from '@capacitor/core'; const originalFetch = window.fetch; @@ -53,14 +52,5 @@ if (Capacitor.getPlatform() === 'android') { return originalXhrOpen.apply(this, args); }; - // Used to toggle rejectUnauthorized in the backend - interfaceAllowInsecureRequests.subscribe(async (isAllowed) => { - if (isAllowed) { - await fetch('http://materialious__allow-insecure-requests'); - } else { - await fetch('http://materialious__deny-insecure-requests'); - } - }); - setTimeout(() => goto('/', { replaceState: true }), 100); } diff --git a/materialious/src/lib/components/ChannelThumbnail.svelte b/materialious/src/lib/components/ChannelThumbnail.svelte index 0d8addc1..0ee518f5 100644 --- a/materialious/src/lib/components/ChannelThumbnail.svelte +++ b/materialious/src/lib/components/ChannelThumbnail.svelte @@ -5,7 +5,7 @@ import { _ } from 'svelte-i18n'; import { get } from 'svelte/store'; import type { Channel } from '../api/model'; - import { truncate } from '../misc'; + import { insecureRequestImageHandler, truncate } from '../misc'; import { interfaceLowBandwidthMode } from '../store'; interface Props { @@ -19,9 +19,9 @@ onMount(async () => { if (get(interfaceLowBandwidthMode)) return; - const img = new Image(); - img.src = proxyGoogleImage(getBestThumbnail(channel.authorThumbnails)); - + const img = await insecureRequestImageHandler( + proxyGoogleImage(getBestThumbnail(channel.authorThumbnails)) + ); img.onload = () => { channelPfp = img; }; diff --git a/materialious/src/lib/components/Comment.svelte b/materialious/src/lib/components/Comment.svelte index 2f3dc9ad..c275d7eb 100644 --- a/materialious/src/lib/components/Comment.svelte +++ b/materialious/src/lib/components/Comment.svelte @@ -4,7 +4,9 @@ import { getBestThumbnail, proxyGoogleImage } from '$lib/images'; import { numberWithCommas } from '$lib/numbers'; import { interfaceLowBandwidthMode } from '$lib/store'; + import { onMount } from 'svelte'; import CommentSelf from './Comment.svelte'; + import { insecureRequestImageHandler } from '$lib/misc'; interface Props { comment: Comment; @@ -36,16 +38,28 @@ return processedHtml; } + + let userPfp = $state(''); + onMount(async () => { + if ($interfaceLowBandwidthMode) return; + const img = await insecureRequestImageHandler( + proxyGoogleImage(getBestThumbnail(comment.authorThumbnails)) + ); + + img.onload = () => { + userPfp = img.src; + }; + });
{#if !$interfaceLowBandwidthMode}
- comment profile + {#if userPfp} + comment profile + {:else} + + {/if}
diff --git a/materialious/src/lib/components/HashtagThumbnail.svelte b/materialious/src/lib/components/HashtagThumbnail.svelte index dfe6ec39..4e4ebbbe 100644 --- a/materialious/src/lib/components/HashtagThumbnail.svelte +++ b/materialious/src/lib/components/HashtagThumbnail.svelte @@ -9,8 +9,6 @@ } let { hashtag }: Props = $props(); - - let channelPfp: HTMLImageElement | undefined; diff --git a/materialious/src/lib/components/Player.svelte b/materialious/src/lib/components/Player.svelte index 6b08ed41..b5c299be 100644 --- a/materialious/src/lib/components/Player.svelte +++ b/materialious/src/lib/components/Player.svelte @@ -23,6 +23,7 @@ import type { VideoPlay } from '../api/model'; import { authStore, + companionStore, instanceStore, playerAndroidLockOrientation, playerAutoPlayStore, @@ -76,7 +77,6 @@ const sessionId = Array.from(Array(16), () => Math.floor(Math.random() * 36).toString(36)).join( '' ); - let isLive = false; let videoPlaybackUstreamerConfig: string | undefined; let serverAbrStreamingUrl: URL | undefined = undefined; let drmParams: string | undefined; @@ -251,7 +251,7 @@ 'language', 'statistics' ], - playbackRates: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3], + playbackRates: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3], enableTooltips: false, seekBarColors: { played: (await getDynamicTheme())['--primary'] @@ -259,8 +259,6 @@ }); if (data.video.fallbackPatch === 'youtubejs' && data.video.ytjs) { - isLive = !!data.video.ytjs.video.basic_info.is_live; - if ( data.video.ytjs.rawApiResponse.data.streamingData && (data.video.ytjs.rawApiResponse.data.streamingData as any).drmParams @@ -343,7 +341,7 @@ type === shaka.net.NetworkingEngine.RequestType.SEGMENT && url.pathname.includes('videoplayback') ) { - if (!isLive) { + if (!data.video.liveNow) { const currentFormat = formatList.find( (format) => fromFormat(format) === (new URL(request.uris[0]).searchParams.get('___key') || '') @@ -632,7 +630,12 @@ await player.attach(playerElement); if (!data.video.hlsUrl) { - let dashUrl = data.video.dashUrl; + let dashUrl: string = ''; + if ($companionStore) { + dashUrl = `${$companionStore}/api/manifest/dash/id/${data.video.videoId}`; + } else { + dashUrl = data.video.dashUrl; + } if (!data.video.fallbackPatch && (!Capacitor.isNativePlatform() || proxyVideos)) { dashUrl += '?local=true'; diff --git a/materialious/src/lib/components/PlaylistThumbnail.svelte b/materialious/src/lib/components/PlaylistThumbnail.svelte index 5b80f2e0..649b7f38 100644 --- a/materialious/src/lib/components/PlaylistThumbnail.svelte +++ b/materialious/src/lib/components/PlaylistThumbnail.svelte @@ -5,7 +5,7 @@ import { _ } from 'svelte-i18n'; import { get } from 'svelte/store'; import type { Playlist } from '../api/model'; - import { truncate } from '../misc'; + import { insecureRequestImageHandler, truncate } from '../misc'; import { interfaceLowBandwidthMode } from '../store'; interface Props { @@ -21,20 +21,23 @@ const playlistLink = `/playlist/${playlist.playlistId}`; - onMount(() => { + onMount(async () => { if (get(interfaceLowBandwidthMode)) return; - img = new Image(); + let imgSrc = ''; + if (playlist.videos.length > 0) { - img.src = getBestThumbnail(playlist.videos[0].videoThumbnails) || ''; + imgSrc = getBestThumbnail(playlist.videos[0].videoThumbnails) || ''; } else if (playlist.playlistThumbnail) { - img.src = playlist.playlistThumbnail; + imgSrc = playlist.playlistThumbnail; } else { - img.src = ''; + imgSrc = ''; loading = false; return; } + img = await insecureRequestImageHandler(imgSrc); + img.onload = () => { loading = false; }; diff --git a/materialious/src/lib/components/Settings/Interface.svelte b/materialious/src/lib/components/Settings/Interface.svelte index dec82064..642f6c6d 100644 --- a/materialious/src/lib/components/Settings/Interface.svelte +++ b/materialious/src/lib/components/Settings/Interface.svelte @@ -13,6 +13,7 @@ import { getPages } from '../../navPages'; import { authStore, + companionStore, darkModeStore, instanceStore, interfaceAllowInsecureRequests, @@ -20,7 +21,6 @@ interfaceAutoExpandComments, interfaceAutoExpandDesc, interfaceDefaultPage, - interfaceDisplayThumbnailAvatars, interfaceForceCase, interfaceLowBandwidthMode, interfaceRegionStore, @@ -31,6 +31,7 @@ } from '../../store'; let invidiousInstance = $state(get(instanceStore)); + let companionInstance = $state(get(companionStore)); let region = $state(get(interfaceRegionStore)); let forceCase = $state(get(interfaceForceCase)); let defaultPage = $state(get(interfaceDefaultPage)); @@ -92,10 +93,28 @@ ui('#dialog-settings'); } + function setCompanion(event: Event) { + event.preventDefault(); + companionStore.set(ensureNoTrailingSlash(companionInstance)); + } + function allowInsecureRequests() { - if (Capacitor.getPlatform() !== 'android') return; + if (!Capacitor.isNativePlatform()) return; interfaceAllowInsecureRequests.set(!$interfaceAllowInsecureRequests); } + + // Used to toggle rejectUnauthorized in the backend + interfaceAllowInsecureRequests.subscribe(async (isAllowed) => { + if (Capacitor.getPlatform() === 'android') { + if (isAllowed) { + await fetch('http://materialious__allow-insecure-requests'); + } else { + await fetch('http://materialious__deny-insecure-requests'); + } + } else if (Capacitor.getPlatform() === 'electron') { + await window.electronAPI.setAllowInsecureSSL(isAllowed); + } + }); {#if Capacitor.isNativePlatform()} @@ -116,24 +135,39 @@ {#if invalidInstance}
{/if} -{/if} -{#if Capacitor.getPlatform() === 'android' && (invalidInstance || $interfaceAllowInsecureRequests)} -
-
{/if} -
- -
-