Merge pull request #941 from Materialious/update/1.9.0

Update/1.9.0
This commit is contained in:
Ward
2025-06-04 17:39:23 +12:00
committed by GitHub
25 changed files with 225 additions and 163 deletions
+35 -10
View File
@@ -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:
<img src="https://github.com/user-attachments/assets/bd7be837-5f08-4ceb-9fdf-f0f445228075" width=15% height=15%>
**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
```
```
+1
View File
@@ -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 && \
+2 -2
View File
@@ -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.
@@ -61,7 +61,11 @@
<release version="1.8.12" date="2025-6-04">
<release version="1.9.0" date="2025-6-04">
<url>https://github.com/Materialious/Materialious/releases/tag/1.9.0</url>
</release>
<release version="1.8.12" date="2025-6-04">
<url>https://github.com/Materialious/Materialious/releases/tag/1.8.12</url>
</release>
<release version="1.8.11" date="2025-6-03">
+9 -6
View File
@@ -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"
}
+3 -3
View File
@@ -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"
]
}
}
+21 -8
View File
@@ -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;
});
+4 -1
View File
@@ -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);
}
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "materialious",
"version": "1.8.12",
"version": "1.9.0",
"private": true,
"scripts": {
"dev": "vite dev",
+1
View File
@@ -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' {} +
+1
View File
@@ -18,6 +18,7 @@ declare global {
requestKey: string,
visitorData: string
) => Promise<string>;
setAllowInsecureSSL: (allowInsecureSSL: boolean) => Promoise<boolean>;
};
}
}
@@ -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);
}
@@ -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;
};
+19 -5
View File
@@ -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;
};
});
</script>
<div class="comment">
{#if !$interfaceLowBandwidthMode}
<div class="comment-header">
<img
class="circle small"
src={proxyGoogleImage(getBestThumbnail(comment.authorThumbnails))}
alt="comment profile"
/>
{#if userPfp}
<img class="circle small" src={userPfp} alt="comment profile" />
{:else}
<progress class="circle"></progress>
{/if}
<div class="comment-info">
<a href={`/channel/${comment.authorId}`} class="author">
<span class="bold" class:channel-owner={comment.authorIsChannelOwner}>
@@ -9,8 +9,6 @@
}
let { hashtag }: Props = $props();
let channelPfp: HTMLImageElement | undefined;
</script>
<a href={hashtag.url} class="wave" style="min-width: 100%;min-height: 100%;">
@@ -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';
@@ -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;
};
@@ -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);
}
});
</script>
{#if Capacitor.isNativePlatform()}
@@ -116,24 +135,39 @@
{#if invalidInstance}
<div style="margin-bottom: 6em;"></div>
{/if}
{/if}
{#if Capacitor.getPlatform() === 'android' && (invalidInstance || $interfaceAllowInsecureRequests)}
<div class="field no-margin">
<nav class="no-padding">
<div class="max">
<div>{$_('layout.allowInsecureRequests')}</div>
{#if Capacitor.isNativePlatform() && (invalidInstance || $interfaceAllowInsecureRequests)}
<div class="field no-margin">
<nav class="no-padding">
<div class="max">
<div>{$_('layout.allowInsecureRequests')}</div>
</div>
<label class="switch">
<input
type="checkbox"
bind:checked={$interfaceAllowInsecureRequests}
onclick={allowInsecureRequests}
/>
<span></span>
</label>
</nav>
</div>
{/if}
<form onsubmit={setCompanion}>
<nav>
<div class="field label border max">
<input bind:value={companionInstance} name="companion-instance" type="text" />
<label for="companion-instance">{$_('layout.companionUrl')}</label>
</div>
<label class="switch">
<input
type="checkbox"
bind:checked={$interfaceAllowInsecureRequests}
onclick={allowInsecureRequests}
/>
<span></span>
</label>
<button class="square round">
<i>done</i>
</button>
</nav>
</div>
</form>
{#if invalidInstance}
<div style="margin-bottom: 6em;"></div>
{/if}
{/if}
<button onclick={toggleDarkMode} class="no-margin">
@@ -171,22 +205,6 @@
</div>
{/if}
<div class="field no-margin">
<nav class="no-padding">
<div class="max">
<div>{$_('layout.displayThumbnailAvatars')}</div>
</div>
<label class="switch">
<input
type="checkbox"
bind:checked={$interfaceDisplayThumbnailAvatars}
onclick={() => interfaceDisplayThumbnailAvatars.set(!$interfaceDisplayThumbnailAvatars)}
/>
<span></span>
</label>
</nav>
</div>
<div class="field no-margin">
<nav class="no-padding">
<div class="max">
@@ -1,18 +1,17 @@
<script lang="ts">
import { getBestThumbnail, proxyGoogleImage } from '$lib/images';
import { getBestThumbnail } from '$lib/images';
import { letterCase } from '$lib/letterCasing';
import { cleanNumber, videoLength } from '$lib/numbers';
import { onDestroy, onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import { get } from 'svelte/store';
import { getChannel, getDeArrow, getThumbnail, getVideoProgress } from '../api';
import { getDeArrow, getThumbnail, getVideoProgress } from '../api';
import type { Notification, PlaylistPageVideo, Video, VideoBase } from '../api/model';
import { truncate } from '../misc';
import { insecureRequestImageHandler, truncate } from '../misc';
import type { PlayerEvents } from '../player';
import {
authStore,
deArrowEnabledStore,
interfaceDisplayThumbnailAvatars,
interfaceLowBandwidthMode,
playerSavePlaybackPositionStore,
syncPartyConnectionsStore,
@@ -29,8 +28,6 @@
let { video = $bindable(), playlistId = '', sideways = $bindable(false) }: Props = $props();
let authorImg: HTMLImageElement | undefined = $state();
let placeholderHeight: number = $state(0);
let watchUrl = new URL(`${location.origin}/watch/${video.videoId}`);
@@ -64,24 +61,9 @@
if (window.innerWidth <= 1500) {
sideways = false;
if (typeof authorImg === 'undefined') loadAuthor();
} else sideways = true;
}
async function loadAuthor() {
if (!get(interfaceDisplayThumbnailAvatars)) return;
try {
const channel = await getChannel(video.authorId, { priority: 'low' });
const loadedPfp = new Image();
loadedPfp.src = proxyGoogleImage(getBestThumbnail(channel.authorThumbnails, 75, 75));
loadedPfp.onload = () => {
authorImg = loadedPfp;
};
} catch {}
}
onMount(async () => {
calcThumbnailPlaceholderHeight();
@@ -95,16 +77,9 @@
if (get(interfaceLowBandwidthMode)) return;
// Load author details in background.
if (!sideways) {
loadAuthor();
}
let imageSrc = getBestThumbnail(video.videoThumbnails) as string;
if (get(deArrowEnabledStore)) {
let locatedThumbnail = false;
try {
const deArrow = await getDeArrow(video.videoId);
for (const title of deArrow.titles) {
@@ -119,16 +94,13 @@
if (thumbnail.timestamp !== null) {
imageSrc = await getThumbnail(video.videoId, thumbnail.timestamp);
}
locatedThumbnail = true;
break;
}
}
} catch {}
}
const img = new Image();
img.src = imageSrc;
const img = await insecureRequestImageHandler(imageSrc);
img.onload = () => {
thumbnail = img;
};
@@ -225,15 +197,6 @@
</div>
<div class="thumbnail-details video-title">
{#if !sideways && !$interfaceLowBandwidthMode && $interfaceDisplayThumbnailAvatars}
<div style="margin-right: 1em;">
{#if authorImg}
<img src={authorImg.src} alt="Author" class="circle small" />
{:else}
<progress style="padding: 15px;" class="circle small"></progress>
{/if}
</div>
{/if}
<div class="video-title">
<a
style="padding-left: 1px;"
@@ -110,6 +110,7 @@
"lowBandwidthMode": "Low bandwidth mode",
"displayThumbnailAvatars": "Thumbnail avatars (Slow)",
"searchHistory": "Search history",
"companionUrl": "Companion URL",
"theme": {
"theme": "Theme",
"darkMode": "Dark mode",
+23 -13
View File
@@ -3,7 +3,7 @@ import { page } from '$app/stores';
import he from 'he';
import type Peer from 'peerjs';
import { get } from 'svelte/store';
import { instanceStore } from './store';
import { instanceStore, interfaceAllowInsecureRequests } from './store';
export function truncate(value: string, maxLength: number = 50): string {
return value.length > maxLength ? `${value.substring(0, maxLength)}...` : value;
@@ -16,10 +16,7 @@ export function decodeHtmlCharCodes(str: string): string {
export function proxyVideoUrl(source: string): string {
const rawSrc = new URL(source);
rawSrc.host = get(instanceStore).replace('http://', '').replace(
'https://',
''
);
rawSrc.host = get(instanceStore).replace('http://', '').replace('https://', '');
return rawSrc.toString();
}
@@ -46,14 +43,11 @@ export async function peerJs(peerId: string): Promise<Peer> {
if (typeof PeerInstance === 'undefined') {
PeerInstance = (await import('peerjs')).Peer;
}
return new PeerInstance(
peerId,
{
host: import.meta.env.VITE_DEFAULT_PEERJS_HOST || '0.peerjs.com',
path: import.meta.env.VITE_DEFAULT_PEERJS_PATH || '/',
port: import.meta.env.VITE_DEFAULT_PEERJS_PORT || 443
}
);
return new PeerInstance(peerId, {
host: import.meta.env.VITE_DEFAULT_PEERJS_HOST || '0.peerjs.com',
path: import.meta.env.VITE_DEFAULT_PEERJS_PATH || '/',
port: import.meta.env.VITE_DEFAULT_PEERJS_PORT || 443
});
}
export function ensureNoTrailingSlash(url: any): string {
@@ -61,3 +55,19 @@ export function ensureNoTrailingSlash(url: any): string {
return url.endsWith('/') ? url.slice(0, -1) : url;
}
export async function insecureRequestImageHandler(source: string): Promise<HTMLImageElement> {
const img = new Image();
if (get(interfaceAllowInsecureRequests)) {
const imgResp = await fetch(source);
if (!imgResp.ok) {
img.src = '';
}
img.src = URL.createObjectURL(await imgResp.blob());
} else {
img.src = source;
}
return img;
}
+11 -2
View File
@@ -5,6 +5,7 @@ import { persisted } from 'svelte-persisted-store';
import { writable, type Writable } from 'svelte/store';
import type { TitleCase } from './letterCasing';
import type { Channel, HashTag, Playlist, PlaylistPageVideo, Video, VideoBase } from './api/model';
import { ensureNoTrailingSlash } from './misc';
function platformDependentDefault(givenValue: any, defaultValue: any): any {
if (typeof givenValue !== 'undefined' && typeof givenValue !== null) {
@@ -17,11 +18,19 @@ function platformDependentDefault(givenValue: any, defaultValue: any): any {
export const instanceStore: Writable<string> = persisted(
'invidiousInstance',
platformDependentDefault(
import.meta.env.VITE_DEFAULT_INVIDIOUS_INSTANCE,
ensureNoTrailingSlash(import.meta.env.VITE_DEFAULT_INVIDIOUS_INSTANCE),
'https://invidious.materialio.us'
)
);
export const companionStore: Writable<string | undefined> = persisted(
'companionInstance',
platformDependentDefault(
ensureNoTrailingSlash(import.meta.env.VITE_DEFAULT_COMPANION_INSTANCE),
undefined
)
);
export const authStore: Writable<null | { username: string; token: string }> = persisted(
'authToken',
null
@@ -118,6 +127,6 @@ export const feedCacheStore: Writable<{
[key: string]: (VideoBase | Video | PlaylistPageVideo)[];
}> = writable({});
export const searchCacheStore: Writable<{
[searchType: string]: (Channel | Video | Playlist | HashTag)[];
[searchTypeAndQuery: string]: (Channel | Video | Playlist | HashTag)[];
}> = writable({});
export const feedLastItemId: Writable<string | undefined> = writable(undefined);
@@ -27,7 +27,7 @@
async function changeType(type: 'playlist' | 'all' | 'video' | 'channel') {
currentType = type;
currentPage = 1;
searchCacheStore.set({ [type]: await getSearch(data.slug, { type: type }) });
searchCacheStore.set({ [data.searchStoreId]: await getSearch(data.slug, { type: type }) });
}
async function loadMore(event: InfiniteEvent) {
@@ -41,7 +41,7 @@
event.detail.complete();
} else {
searchCacheStore.set({
[currentType]: [...($searchCacheStore[currentType] ?? []), ...newSearch]
[data.searchStoreId]: [...($searchCacheStore[data.searchStoreId] ?? []), ...newSearch]
});
event.detail.loaded();
}
@@ -81,11 +81,11 @@
</div>
</div>
{#if $searchCacheStore[currentType].length > 0}
{#if $searchCacheStore[data.searchStoreId]}
<div class="page right active">
<div class="space"></div>
<div class="grid">
{#each $searchCacheStore[currentType] as item}
{#each $searchCacheStore[data.searchStoreId] as item}
<ContentColumn>
{#key item}
<article
@@ -13,24 +13,26 @@ export async function load({ params, url }) {
type = 'all';
}
const search = get(searchCacheStore).all;
const searchStoreId = type + params.slug;
const search = get(searchCacheStore)[searchStoreId];
if (!search) {
try {
searchCacheStore.set({ [type]: await getSearch(params.slug, { type: type }) });
searchCacheStore.set({ [searchStoreId]: await getSearch(params.slug, { type: type }) });
} catch (errorMessage: any) {
error(500, errorMessage);
}
} else {
getSearch(params.slug, { type: type }).then((newSearch) => {
searchCacheStore.set({
[type]: [...new Set([...newSearch, ...search])]
[searchStoreId]: [...new Set([...newSearch, ...search])]
});
});
}
return {
slug: params.slug,
searchType: type
searchType: type,
searchStoreId: searchStoreId
};
}
+1 -1
View File
@@ -3,7 +3,7 @@ import os
import re
from datetime import datetime
LATEST_VERSION = "1.8.12"
LATEST_VERSION = "1.9.0"
RELEASE_DATE = datetime.now().strftime("%Y-%-m-%d") # Format: YYYY-M-D
WORKING_DIR = os.path.join(