Merge pull request #1458 from Materialious/update/1.15.9

Update/1.15.9
This commit is contained in:
Ward
2026-02-17 20:44:59 +13:00
committed by GitHub
19 changed files with 104 additions and 111 deletions
+11
View File
@@ -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
+2 -2
View File
@@ -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.
@@ -81,7 +81,11 @@
<release version="1.15.8" date="2026-2-17">
<release version="1.15.9" date="2026-2-17">
<url>https://github.com/Materialious/Materialious/releases/tag/1.15.9</url>
</release>
<release version="1.15.8" date="2026-2-17">
<url>https://github.com/Materialious/Materialious/releases/tag/1.15.8</url>
</release>
<release version="1.15.7" date="2026-2-16">
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "materialious",
"version": "1.15.8",
"version": "1.15.9",
"private": true,
"scripts": {
"dev": "npm run patch:github && vite dev",
@@ -137,27 +137,25 @@ export async function parseChannelRSS(channelId: string): Promise<void> {
}
}
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() {
+5 -13
View File
@@ -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(
@@ -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();
}
@@ -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<string, unknown>, 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);
+8 -8
View File
@@ -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
};
}
+3
View File
@@ -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');
+3 -1
View File
@@ -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'
};
}
+8 -26
View File
@@ -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<string | undefined> = 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<string | null | undefined> = 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<string | null | undefined> = 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<string | null | undefined> = 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<boolean> = 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'
);
@@ -81,22 +81,24 @@
{/if}
</div>
<article
class="surface-container-highest no-padding"
style="width: 100%;height: fit-content;"
>
<altcha-widget
challengeurl="/api/captcha"
hidelogo
hidefooter
onstatechange={(ev) => {
const { payload, state } = ev.detail;
if (state === 'verified' && payload) {
captchaPayload = payload;
}
}}
></altcha-widget>
</article>
{#if !isOwnBackend()?.captchaDisabled}
<article
class="surface-container-highest no-padding"
style="width: 100%;height: fit-content;"
>
<altcha-widget
challengeurl="/api/captcha"
hidelogo
hidefooter
onstatechange={(ev) => {
const { payload, state } = ev.detail;
if (state === 'verified' && payload) {
captchaPayload = payload;
}
}}
></altcha-widget>
</article>
{/if}
<nav class="right-align">
<button
@@ -1,5 +1,6 @@
import { isOwnBackend } from '$lib/shared';
import { env } from '$env/dynamic/public';
import { env as privateEnv } from '$env/dynamic/private';
import { error } from '@sveltejs/kit';
import { parse as tldParse } from 'tldts';
@@ -14,6 +15,12 @@ const allowedBaseDomains: string[] = [
'ajay.app'
];
if (privateEnv.WHITELIST_BASE_DOMAIN) {
for (const baseDomain of privateEnv.WHITELIST_BASE_DOMAIN.split(',')) {
if (baseDomain) allowedBaseDomains.push(baseDomain);
}
}
const dynamicAllowDomainsEnvVars = [
env.PUBLIC_DEFAULT_DEARROW_THUMBNAIL_INSTANCE,
env.PUBLIC_DEFAULT_DEARROW_INSTANCE,
@@ -73,18 +80,7 @@ async function proxyRequest(
}
}
const requestHeaders = new Headers(request.headers);
for (const key of [
'referer',
'x-forwarded-for',
'x-requested-with',
'sec-ch-ua-mobile',
'sec-ch-ua',
'sec-ch-ua-platform',
'content-length'
]) {
requestHeaders.delete(key);
}
const requestHeaders = new Headers();
requestHeaders.set('host', urlToProxyObj.host);
requestHeaders.set('origin', urlToProxyObj.origin);
@@ -123,7 +119,7 @@ async function proxyRequest(
throw error(500, errorMsg);
}
const responseHeaders = new Headers(response.headers);
const responseHeaders = new Headers();
responseHeaders.set('transfer-encoding', 'chunked');
responseHeaders.delete('content-encoding');
responseHeaders.set('access-control-allow-origin', request.headers.get('origin') ?? '');
@@ -16,7 +16,7 @@ const zUserCreate = z.object({
cipher: z.string().max(255),
nonce: z.string().max(255)
}),
captchaPayload: z.string()
captchaPayload: z.string().optional()
});
export async function POST({ request, cookies, locals }) {
@@ -28,7 +28,7 @@ export async function POST({ request, cookies, locals }) {
if (!userToCreate.success) throw error(400);
await verifyCaptcha(userToCreate.data.captchaPayload, locals.captchaKey, 1);
await verifyCaptcha(userToCreate.data.captchaPayload ?? '', locals.captchaKey, 1);
const createdUser = await createUser({
username: userToCreate.data.username,
@@ -8,7 +8,7 @@ import { verifyCaptcha } from '$lib/server/captcha';
const zUserLogin = z.object({
username: z.string(),
passwordHash: z.string(),
captchaPayload: z.string()
captchaPayload: z.string().optional()
});
export async function POST({ request, cookies, locals }) {
@@ -20,7 +20,7 @@ export async function POST({ request, cookies, locals }) {
if (!userLogin.success) throw error(401);
await verifyCaptcha(userLogin.data.captchaPayload, locals.captchaKey, 1);
await verifyCaptcha(userLogin.data.captchaPayload ?? '', locals.captchaKey, 1);
const userModel = await authenticateUser(userLogin.data.username, userLogin.data.passwordHash);
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -3,7 +3,7 @@ import os
import re
from datetime import datetime
LATEST_VERSION = "1.15.8"
LATEST_VERSION = "1.15.9"
RELEASE_DATE = datetime.now().strftime("%Y-%-m-%d") # Format: YYYY-M-D
WORKING_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "materialious")