diff --git a/docs/DOCKER-FULL.md b/docs/DOCKER-FULL.md index 73f21cd8..728ff4c6 100644 --- a/docs/DOCKER-FULL.md +++ b/docs/DOCKER-FULL.md @@ -12,6 +12,9 @@ invidious_companion: public_url: "http://companion.example.com/companion" ``` +### Proof-of-work Captcha +Will only work while using HTTPS. If in HTTP set `PUBLIC_CAPTCHA_DISABLED` to `true` + ### Docker Compose ```yaml @@ -42,6 +45,14 @@ services: # Allow anyone to register PUBLIC_REGISTRATION_ALLOWED: "false" + # Disable POW Captcha + PUBLIC_CAPTCHA_DISABLED: "false" + + # Allows you to whitelist additional domains in proxy. + # Should be comma separated and be the base domain + # e.g. "youtube.com,google.com" NOT "https://youtube.com,https://videos.google.com" + WHITELIST_BASE_DOMAIN: "" + # Allow any domain in proxy # This shouldn't be used unless you KNOW what your doing # requires VITE_REGISTRATION_ALLOWED to be false diff --git a/materialious/android/app/build.gradle b/materialious/android/app/build.gradle index 0369d805..9c76dc20 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 216 - versionName "1.15.8" + versionCode 217 + versionName "1.15.9" 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 34c43a34..de438669 100644 --- a/materialious/electron/materialious.metainfo.xml +++ b/materialious/electron/materialious.metainfo.xml @@ -81,7 +81,11 @@ - + + + https://github.com/Materialious/Materialious/releases/tag/1.15.9 + + https://github.com/Materialious/Materialious/releases/tag/1.15.8 diff --git a/materialious/electron/package.json b/materialious/electron/package.json index 49734948..2f30b3bd 100644 --- a/materialious/electron/package.json +++ b/materialious/electron/package.json @@ -1,6 +1,6 @@ { "name": "Materialious", - "version": "1.15.8", + "version": "1.15.9", "description": "Modern material design for YouTube and Invidious.", "author": { "name": "Ward Pearce", diff --git a/materialious/package.json b/materialious/package.json index 8d3ec302..223b4c79 100644 --- a/materialious/package.json +++ b/materialious/package.json @@ -1,6 +1,6 @@ { "name": "materialious", - "version": "1.15.8", + "version": "1.15.9", "private": true, "scripts": { "dev": "npm run patch:github && vite dev", diff --git a/materialious/src/lib/api/youtubejs/subscriptions.ts b/materialious/src/lib/api/youtubejs/subscriptions.ts index 81499cce..97407631 100644 --- a/materialious/src/lib/api/youtubejs/subscriptions.ts +++ b/materialious/src/lib/api/youtubejs/subscriptions.ts @@ -137,27 +137,25 @@ export async function parseChannelRSS(channelId: string): Promise { } } - updateLastRssFetch(channelId, channelName ?? 'Unknown'); + await updateLastRssFetch(channelId, channelName ?? 'Unknown'); } async function updateLastRssFetch(channelId: string, channelName: string) { - localDb.channelSubscriptions + const subscription = await localDb.channelSubscriptions .where('channelId') .equals(channelId) - .first() - .then((subscription) => { - if (subscription) { - localDb.channelSubscriptions.where('channelId').equals(channelId).modify({ - lastRSSFetch: new Date() - }); - } else { - localDb.channelSubscriptions.add({ - channelId: channelId, - channelName: channelName, - lastRSSFetch: new Date() - }); - } + .first(); + if (subscription) { + await localDb.channelSubscriptions.where('channelId').equals(channelId).modify({ + lastRSSFetch: new Date() }); + } else { + await localDb.channelSubscriptions.add({ + channelId: channelId, + channelName: channelName, + lastRSSFetch: new Date() + }); + } } export async function clearFeedYTjs() { diff --git a/materialious/src/lib/components/Player.svelte b/materialious/src/lib/components/Player.svelte index 8d635b7f..e5f10ba5 100644 --- a/materialious/src/lib/components/Player.svelte +++ b/materialious/src/lib/components/Player.svelte @@ -46,7 +46,6 @@ } from '../store'; import { setStatusBarColor } from '../theme'; import { getVideoYTjs } from '$lib/api/youtubejs/video'; - import { env } from '$env/dynamic/public'; import { goToNextVideo, goToPreviousVideo, @@ -60,7 +59,7 @@ import { Network, type ConnectionStatus } from '@capacitor/network'; import { fade } from 'svelte/transition'; import { addToast } from './Toast.svelte'; - import { isMobile, isUnrestrictedPlatform, isYTBackend, truncate } from '$lib/misc'; + import { getPublicEnv, isMobile, isUnrestrictedPlatform, isYTBackend, truncate } from '$lib/misc'; import { generateThumbnailWebVTT, drawTimelineThumbnail, @@ -515,11 +514,8 @@ // Due to CORs issues with redirects, hosted instances of Materialious // dirctly provide the companion instance // while clients can just use the reirect provided by Invidious' API - if ( - import.meta.env.VITE_DEFAULT_COMPANION_INSTANCE || - env.PUBLIC_DEFAULT_COMPANION_INSTANCE - ) { - dashUrl = `${import.meta.env.VITE_DEFAULT_COMPANION_INSTANCE || env.PUBLIC_DEFAULT_COMPANION_INSTANCE}/api/manifest/dash/id/${data.video.videoId}`; + if (getPublicEnv('DEFAULT_COMPANION_INSTANCE')) { + dashUrl = `${getPublicEnv('DEFAULT_COMPANION_INSTANCE')}/api/manifest/dash/id/${data.video.videoId}`; } else { if (!data.video.dashUrl) { error(500, 'No dash manifest found'); @@ -582,16 +578,12 @@ if (data.video.captions) { for (const caption of data.video.captions) { let captionUrl: string; - if ( - !import.meta.env.VITE_DEFAULT_COMPANION_INSTANCE && - !env.PUBLIC_DEFAULT_COMPANION_INSTANCE && - $invidiousInstanceStore - ) { + if (!getPublicEnv('DEFAULT_COMPANION_INSTANCE') && $invidiousInstanceStore) { captionUrl = caption.url.startsWith('http') ? caption.url : `${new URL($invidiousInstanceStore).origin}${caption.url}`; } else { - captionUrl = `${import.meta.env.VITE_DEFAULT_COMPANION_INSTANCE || env.PUBLIC_DEFAULT_COMPANION_INSTANCE}${caption.url}`; + captionUrl = `${getPublicEnv('DEFAULT_COMPANION_INSTANCE')}${caption.url}`; } await player.addTextTrackAsync( diff --git a/materialious/src/lib/components/settings/Interface.svelte b/materialious/src/lib/components/settings/Interface.svelte index d06b6e1b..a248efc9 100644 --- a/materialious/src/lib/components/settings/Interface.svelte +++ b/materialious/src/lib/components/settings/Interface.svelte @@ -13,7 +13,8 @@ isUnrestrictedPlatform, setInvidiousInstance, goToInvidiousLogin, - invidiousLogout + invidiousLogout, + timeout } from '../../misc'; import { getPages, type Pages } from '../../navPages'; import ColorPicker from 'svelte-awesome-color-picker'; @@ -82,12 +83,16 @@ async function setInstance(event: Event) { event.preventDefault(); invalidInstance = !(await setInvidiousInstance(invidiousInstance)); + + await timeout(100); location.reload(); } async function setBackend(event: Event) { const select = event.target as HTMLSelectElement; backendInUseStore.set(select.value as 'ivg' | 'yt'); + + await timeout(100); location.reload(); } diff --git a/materialious/src/lib/externalSettings/index.ts b/materialious/src/lib/externalSettings/index.ts index a69b19c5..bb30b7d3 100644 --- a/materialious/src/lib/externalSettings/index.ts +++ b/materialious/src/lib/externalSettings/index.ts @@ -2,12 +2,12 @@ import { page } from '$app/state'; import { get } from 'svelte/store'; import { z } from 'zod'; -import { env } from '$env/dynamic/public'; import { persistedStores } from './settings'; import { isOwnBackend } from '$lib/shared'; import { addOrUpdateKeyValue, getKeyValue } from '$lib/api/backend'; import { rawMasterKeyStore } from '$lib/store'; +import { getPublicEnv } from '$lib/misc'; export async function syncSettingsToBackend() { if (!isOwnBackend() || !get(rawMasterKeyStore)) return; @@ -74,13 +74,11 @@ function setStores(toSet: Record, overwriteExisting = false) { } export function loadSettingsFromEnv() { - if ( - typeof import.meta.env.VITE_DEFAULT_SETTINGS !== 'string' && - typeof env.PUBLIC_DEFAULT_SETTINGS !== 'string' - ) - return; + const defaultSettings = getPublicEnv('DEFAULT_SETTINGS'); - let raw = import.meta.env.VITE_DEFAULT_SETTINGS || env.PUBLIC_DEFAULT_SETTINGS; + if (typeof defaultSettings !== 'string') return; + + let raw = defaultSettings; // Docker wraps env vars in quotes if (raw.startsWith('"')) raw = raw.slice(1); diff --git a/materialious/src/lib/misc.ts b/materialious/src/lib/misc.ts index aeab6717..02edcb0c 100644 --- a/materialious/src/lib/misc.ts +++ b/materialious/src/lib/misc.ts @@ -34,17 +34,18 @@ import { isOwnBackend } from './shared'; import { Browser } from '@capacitor/browser'; import { clearFeedYTjs } from './api/youtubejs/subscriptions'; +export function getPublicEnv(envName: string): string | undefined { + return env[`PUBLIC_${envName}`] ?? import.meta.env[`VITE_${envName}`]; +} + export function isMobile(): boolean { const userAgent = navigator.userAgent; const hasTouchSupport = 'ontouchstart' in window || navigator.maxTouchPoints > 0; - const isSmallScreen = window.innerWidth < 768; const isUserAgentMobile = /Mobi|Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent); - return ( - (hasTouchSupport && isSmallScreen && isUserAgentMobile) || Capacitor.getPlatform() === 'android' - ); + return (hasTouchSupport && isUserAgentMobile) || Capacitor.getPlatform() === 'android'; } export function isVideoID(videoId: string): boolean { @@ -86,10 +87,9 @@ export interface PeerInstance { export function determinePeerJsInstance(): PeerInstance { return { - host: - import.meta.env.VITE_DEFAULT_PEERJS_HOST || env.PUBLIC_DEFAULT_PEERJS_HOST || '0.peerjs.com', - path: import.meta.env.VITE_DEFAULT_PEERJS_PATH || env.PUBLIC_DEFAULT_PEERJS_PATH || '/', - port: import.meta.env.VITE_DEFAULT_PEERJS_PORT || env.PUBLIC_DEFAULT_PEERJS_PORT || 443 + host: getPublicEnv('DEFAULT_PEERJS_HOST') || '0.peerjs.com', + path: getPublicEnv('DEFAULT_PEERJS_PATH') || '/', + port: getPublicEnv('DEFAULT_PEERJS_PORT') ? Number(getPublicEnv('DEFAULT_PEERJS_PORT')) : 443 }; } diff --git a/materialious/src/lib/server/captcha.ts b/materialious/src/lib/server/captcha.ts index a8636118..4b1b5cdc 100644 --- a/materialious/src/lib/server/captcha.ts +++ b/materialious/src/lib/server/captcha.ts @@ -1,8 +1,11 @@ import { error } from '@sveltejs/kit'; import { verifySolution } from 'altcha-lib'; import { getSequelize } from './database'; +import { isOwnBackend } from '$lib/shared'; export async function verifyCaptcha(payload: string, key: string, maxUses: number = -1) { + if (isOwnBackend()?.captchaDisabled) return; + const passedCaptcha = await verifySolution(payload, key, true); if (!passedCaptcha) { throw error(400, 'Unsupported payload'); diff --git a/materialious/src/lib/shared/index.ts b/materialious/src/lib/shared/index.ts index e8d6a5f8..de9f35be 100644 --- a/materialious/src/lib/shared/index.ts +++ b/materialious/src/lib/shared/index.ts @@ -6,6 +6,7 @@ export type IsOwnBackend = { requireAuth: boolean; registrationAllowed: boolean; allowAnyProxy: boolean; + captchaDisabled: boolean; }; export function isOwnBackend(): IsOwnBackend | null { @@ -16,6 +17,7 @@ export function isOwnBackend(): IsOwnBackend | null { internalAuth: env.PUBLIC_INTERNAL_AUTH !== 'false', requireAuth: env.PUBLIC_REQUIRE_AUTH !== 'false', registrationAllowed: env.PUBLIC_REGISTRATION_ALLOWED === 'true', - allowAnyProxy: env.PUBLIC_DANGEROUS_ALLOW_ANY_PROXY === 'true' + allowAnyProxy: env.PUBLIC_DANGEROUS_ALLOW_ANY_PROXY === 'true', + captchaDisabled: env.PUBLIC_CAPTCHA_DISABLED === 'true' }; } diff --git a/materialious/src/lib/store.ts b/materialious/src/lib/store.ts index 2b985f69..4b168c42 100644 --- a/materialious/src/lib/store.ts +++ b/materialious/src/lib/store.ts @@ -3,7 +3,6 @@ import type Peer from 'peerjs'; import type { DataConnection } from 'peerjs'; import { writable, type Writable } from 'svelte/store'; import { Preferences } from '@capacitor/preferences'; -import { env } from '$env/dynamic/public'; import { persist, createLocalStorage, @@ -23,7 +22,7 @@ import type { VideoPlay } from './api/model'; import type { ParsedDescription } from './description'; -import { ensureNoTrailingSlash } from './misc'; +import { ensureNoTrailingSlash, getPublicEnv } from './misc'; import type { EngineFallback } from './api/misc'; function createListenerFunctions(): { @@ -89,11 +88,9 @@ function ifNotWebDefault(givenValue: any, defaultValue: any): any { export const invidiousInstanceStore: Writable = persist( writable( - !import.meta.env.VITE_DEFAULT_INVIDIOUS_INSTANCE || !env.PUBLIC_DEFAULT_INVIDIOUS_INSTANCE + !getPublicEnv('DEFAULT_INVIDIOUS_INSTANCE') ? undefined - : ensureNoTrailingSlash( - import.meta.env.VITE_DEFAULT_INVIDIOUS_INSTANCE || env.PUBLIC_DEFAULT_INVIDIOUS_INSTANCE - ) + : ensureNoTrailingSlash(getPublicEnv('DEFAULT_INVIDIOUS_INSTANCE')) ), createStorage(), 'invidiousInstance' @@ -198,8 +195,7 @@ export const returnYtDislikesStore = persist(writable(false), createStorage(), ' export const returnYTDislikesInstanceStore: Writable = persist( writable( ifNotWebDefault( - import.meta.env.VITE_DEFAULT_RETURNYTDISLIKES_INSTANCE || - env.PUBLIC_DEFAULT_RETURNYTDISLIKES_INSTANCE, + getPublicEnv('DEFAULT_RETURNYTDISLIKES_INSTANCE'), 'https://ryd-proxy.materialio.us' ) ), @@ -211,9 +207,7 @@ export const synciousStore = persist(writable(false), createStorage(), 'syncious export const synciousInstanceStore: Writable = persist( writable( ifNotWebDefault( - import.meta.env.VITE_DEFAULT_SYNCIOUS_INSTANCE || - import.meta.env.VITE_DEFAULT_API_EXTENDED_INSTANCE || - env.PUBLIC_DEFAULT_RETURNYTDISLIKES_INSTANCE, + getPublicEnv('DEFAULT_SYNCIOUS_INSTANCE') || getPublicEnv('DEFAULT_API_EXTENDED_INSTANCE'), 'https://extended-api.materialio.us' ) ), @@ -286,11 +280,7 @@ export const interfaceAndroidUseNativeShare = persist( export const sponsorBlockStore = persist(writable(true), createStorage(), 'sponsorBlock'); export const sponsorBlockUrlStore: Writable = persist( - writable( - import.meta.env.VITE_DEFAULT_SPONSERBLOCK_INSTANCE || - env.PUBLIC_DEFAULT_SPONSERBLOCK_INSTANCE || - 'https://sponsor.ajay.app' - ), + writable(getPublicEnv('DEFAULT_SPONSERBLOCK_INSTANCE') || 'https://sponsor.ajay.app'), createStorage(), 'sponsorBlockUrl' ); @@ -311,22 +301,14 @@ export const sponsorBlockTimelineStore: Writable = persist( ); export const deArrowInstanceStore = persist( - writable( - import.meta.env.VITE_DEFAULT_DEARROW_INSTANCE || - env.PUBLIC_DEFAULT_DEARROW_INSTANCE || - 'https://sponsor.ajay.app' - ), + writable(getPublicEnv('DEFAULT_DEARROW_INSTANCE') || 'https://sponsor.ajay.app'), createStorage(), 'deArrowInstance' ); export const deArrowEnabledStore = persist(writable(false), createStorage(), 'deArrowEnabled'); export const deArrowTitlesOnly = persist(writable(true), createStorage(), 'deArrowTitlesOnly'); export const deArrowThumbnailInstanceStore = persist( - writable( - import.meta.env.VITE_DEFAULT_DEARROW_THUMBNAIL_INSTANCE || - env.PUBLIC_DEFAULT_DEARROW_THUMBNAIL_INSTANCE || - 'https://dearrow-thumb.ajay.app' - ), + writable(getPublicEnv('DEFAULT_DEARROW_THUMBNAIL_INSTANCE') || 'https://dearrow-thumb.ajay.app'), createStorage(), 'deArrowThumbnailInstance' ); diff --git a/materialious/src/routes/(app)/internal/login/+page.svelte b/materialious/src/routes/(app)/internal/login/+page.svelte index bf6c5af9..ef374b6d 100644 --- a/materialious/src/routes/(app)/internal/login/+page.svelte +++ b/materialious/src/routes/(app)/internal/login/+page.svelte @@ -81,22 +81,24 @@ {/if} -
- { - const { payload, state } = ev.detail; - if (state === 'verified' && payload) { - captchaPayload = payload; - } - }} - > -
+ {#if !isOwnBackend()?.captchaDisabled} +
+ { + const { payload, state } = ev.detail; + if (state === 'verified' && payload) { + captchaPayload = payload; + } + }} + > +
+ {/if}