Merge pull request #980 from Materialious/update/1.9.9

Update/1.9.9
This commit is contained in:
Ward
2025-06-19 15:46:16 +12:00
committed by GitHub
31 changed files with 366 additions and 131 deletions
+4
View File
@@ -19,6 +19,7 @@
- [Invidious companion support.](./docs/DOCKER.md#invidious-companion-support)
- [Invidious API extended integration.](https://github.com/Materialious/api-extended)
- [YouTube.js](https://github.com/LuanRT/YouTube.js) fallback if Invidious fails loading videos for Desktop & Android.
- Android TV support
- Support for disabling certificate validation for homelab users.
- Sync your watch progress between Invidious sessions.
- Watch sync parties.
@@ -203,6 +204,9 @@ Help [translate Materialious via Weblate](https://toolate.othing.xyz/projects/ma
## Mobile
<img src="./previews/mobile-preview.png" style="height: 500px"/>
## Android TV
![Preview of subscriptions page on Android TV](./previews/android-tv.png)
## Player
![Preview of player](./previews/player-preview.png)
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

+6 -3
View File
@@ -7,14 +7,17 @@ android {
applicationId "us.materialio.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 125
versionName "1.9.8"
versionCode 126
versionName "1.9.9"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86_64'
}
}
buildTypes {
release {
@@ -26,7 +29,7 @@ android {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
include 'armeabi-v7a', 'arm64-v8a', 'x86_64'
universalApk true
}
}
@@ -3,10 +3,12 @@
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:banner="@drawable/banner"
android:label="@string/app_name"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:name=".MainActivity"
@@ -17,6 +19,7 @@
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
@@ -0,0 +1,24 @@
package us.materialio.app;
import android.app.UiModeManager;
import android.content.Context;
import android.content.res.Configuration;
import com.getcapacitor.annotation.CapacitorPlugin;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.PluginCall;
import com.getcapacitor.Plugin;
import com.getcapacitor.JSObject;
@CapacitorPlugin(name = "AndroidTv")
public class AndroidTv extends Plugin {
@PluginMethod()
public void isAndroidTv(PluginCall call) {
UiModeManager uiModeManager = (UiModeManager) getContext().getSystemService(Context.UI_MODE_SERVICE);
boolean isTelevision = uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
JSObject ret = new JSObject();
ret.put("value", isTelevision);
call.resolve(ret);
}
}
@@ -9,6 +9,7 @@ public class MainActivity extends BridgeActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
registerPlugin(ColorTheme.class);
registerPlugin(AndroidTv.class);
super.onCreate(savedInstanceState);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

+12 -12
View File
@@ -1,18 +1,18 @@
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'us.materialio.app',
appName: 'Materialious',
webDir: 'build',
plugins: {
CapacitorNodeJS: {
nodeDir: 'nodejs-android',
}
},
server: {
hostname: 'www.youtube.com',
androidScheme: 'https'
}
appId: 'us.materialio.app',
appName: 'Materialious',
webDir: 'build',
plugins: {
CapacitorNodeJS: {
nodeDir: 'nodejs-android'
}
},
server: {
hostname: 'www.youtube.com',
androidScheme: 'https'
}
};
export default config;
+12 -12
View File
@@ -1,18 +1,18 @@
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'us.materialio.app',
appName: 'Materialious',
webDir: 'build',
plugins: {
CapacitorNodeJS: {
nodeDir: 'nodejs-android',
}
},
server: {
hostname: 'www.youtube.com',
androidScheme: 'https'
}
appId: 'us.materialio.app',
appName: 'Materialious',
webDir: 'build',
plugins: {
CapacitorNodeJS: {
nodeDir: 'nodejs-android'
}
},
server: {
hostname: 'www.youtube.com',
androidScheme: 'https'
}
};
export default config;
@@ -60,7 +60,11 @@
<release version="1.9.8" date="2025-6-18">
<release version="1.9.9" date="2025-6-19">
<url>https://github.com/Materialious/Materialious/releases/tag/1.9.9</url>
</release>
<release version="1.9.8" date="2025-6-18">
<url>https://github.com/Materialious/Materialious/releases/tag/1.9.8</url>
</release>
<release version="1.9.7" date="2025-6-15">
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "Materialious",
"version": "1.9.8",
"version": "1.9.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "Materialious",
"version": "1.9.8",
"version": "1.9.9",
"license": "MIT",
"dependencies": {
"@capacitor-community/electron": "^5.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Materialious",
"version": "1.9.8",
"version": "1.9.9",
"description": "Modern material design for Invidious.",
"author": {
"name": "Ward Pearce",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "materialious",
"version": "1.9.8",
"version": "1.9.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "materialious",
"version": "1.9.8",
"version": "1.9.9",
"hasInstallScript": true,
"dependencies": {
"@capacitor-community/electron": "^5.0.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "materialious",
"version": "1.9.8",
"version": "1.9.9",
"private": true,
"scripts": {
"dev": "vite dev",
@@ -1,5 +1,6 @@
import { goto } from '$app/navigation';
import { Capacitor } from '@capacitor/core';
import { NodeJS } from 'capacitor-nodejs';
const originalFetch = window.fetch;
const corsProxyUrl: string = 'http://localhost:3000/';
@@ -52,5 +53,7 @@ if (Capacitor.getPlatform() === 'android') {
return originalXhrOpen.apply(this, args);
};
setTimeout(() => goto('/', { replaceState: true }), 100);
NodeJS.whenReady().then(() => {
goto('/', { replaceState: true });
});
}
@@ -0,0 +1,18 @@
import { registerPlugin } from '@capacitor/core';
import { WebPlugin } from '@capacitor/core';
interface AndroidTV {
isAndroidTv: () => Promise<boolean>;
}
export class AndroidTvWeb extends WebPlugin implements AndroidTV {
async isAndroidTv(): Promise<boolean> {
return false;
}
}
const androidTv = registerPlugin<AndroidTV>('AndroidTv', {
web: new AndroidTvWeb()
});
export default androidTv;
@@ -1,28 +1,27 @@
import { registerPlugin } from "@capacitor/core";
import { registerPlugin } from '@capacitor/core';
export interface ColorTheme {
getColorPalette: () => Promise<{
primary: number;
onPrimary: number;
secondary: number;
accent: number;
}>;
getColorPalette: () => Promise<{
primary: number;
onPrimary: number;
secondary: number;
accent: number;
}>;
}
export function convertToHexColorCode(color: number): string {
// Convert the negative number to a positive unsigned integer
const unsignedColor = color < 0 ? color + 0x100000000 : color;
// Convert the negative number to a positive unsigned integer
const unsignedColor = color < 0 ? color + 0x100000000 : color;
// Extract the RGB components
const r = (unsignedColor >> 16) & 0xff;
const g = (unsignedColor >> 8) & 0xff;
const b = unsignedColor & 0xff;
// Extract the RGB components
const r = (unsignedColor >> 16) & 0xff;
const g = (unsignedColor >> 8) & 0xff;
const b = unsignedColor & 0xff;
// Convert to hex format and pad with zeros if necessary
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()}`;
// Convert to hex format and pad with zeros if necessary
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()}`;
}
const colorTheme = registerPlugin<ColorTheme>('ColorTheme');
export default colorTheme;
export default colorTheme;
+7 -2
View File
@@ -59,8 +59,13 @@ export async function fetchErrorHandle(response: Response): Promise<Response> {
return response;
}
export function buildAuthHeaders(): { headers: { Authorization: string } } {
return { headers: { Authorization: `Bearer ${get(authStore)?.token}` } };
export function buildAuthHeaders(): { headers: Record<string, string> } {
const authToken = get(authStore)?.token ?? '';
if (authToken.startsWith('SID=')) {
return { headers: { __sid_auth: authToken } };
} else {
return { headers: { Authorization: `Bearer ${get(authStore)?.token}` } };
}
}
export async function getTrending(fetchOptions?: RequestInit): Promise<Video[]> {
@@ -1,25 +1,30 @@
<script lang="ts">
import androidTv from '$lib/android/plugins/androidTv';
import { onMount } from 'svelte';
let { children } = $props();
let largeCol = $state('2');
let smallCol = $state('12');
let mediumCol = $state('6');
function checkWidth() {
if (innerWidth <= 1750) {
async function checkWidth() {
if (await androidTv.isAndroidTv()) {
mediumCol = '3';
} else if (innerWidth <= 1750) {
largeCol = '4';
} else {
largeCol = '2';
}
}
onMount(() => {
checkWidth();
onMount(async () => {
await checkWidth();
addEventListener('resize', () => checkWidth());
});
</script>
<div class="s12 m6 l{largeCol}">
<div class="s{smallCol} m{mediumCol} l{largeCol}">
{@render children?.()}
</div>
+29 -9
View File
@@ -47,6 +47,7 @@
import { patchYoutubeJs } from '$lib/patches/youtubejs';
import { playbackRates } from '$lib/const';
import { EndTimeElement } from '$lib/shaka-elements/endTime';
import androidTv from '$lib/android/plugins/androidTv';
interface Props {
data: { video: VideoPlay; content: PhasedDescription; playlistId: string | null };
@@ -67,7 +68,6 @@
let playerPosSet = false;
let originalOrigination: ScreenOrientationResult | undefined;
let watchProgressTimeout: NodeJS.Timeout;
let userWantsFullscreen = false;
let playerElementResizeObserver: ResizeObserver | undefined;
let showVideoRetry = $state(false);
@@ -188,7 +188,11 @@
}
async function androidHandleRotate() {
if (Capacitor.getPlatform() === 'android' && data.video.adaptiveFormats.length > 0) {
if (
Capacitor.getPlatform() === 'android' &&
data.video.adaptiveFormats.length > 0 &&
!(await androidTv.isAndroidTv())
) {
const videoFormats = data.video.adaptiveFormats.filter((format) =>
format.type.startsWith('video/')
);
@@ -479,7 +483,6 @@
// Required to stop buttons from being still selected when fullscreening
document.addEventListener('fullscreenchange', async () => {
userWantsFullscreen = document.fullscreenElement !== null;
const buttons = document.querySelectorAll('.shaka-controls-button-panel button');
buttons.forEach((button) => {
// Reset the button's focus and active states
@@ -513,7 +516,7 @@
playerElement.pause();
}
if (!document.fullscreenElement && userWantsFullscreen) {
if (!document.fullscreenElement) {
shakaUi.getControls()?.toggleFullScreen();
}
return false;
@@ -523,7 +526,7 @@
if (!playerElement) return;
playerElement.currentTime = playerElement.currentTime + 10;
if (!document.fullscreenElement && userWantsFullscreen) {
if (!document.fullscreenElement) {
shakaUi.getControls()?.toggleFullScreen();
}
return false;
@@ -534,7 +537,7 @@
playerElement.currentTime = playerElement.currentTime - 10;
if (!document.fullscreenElement && userWantsFullscreen) {
if (!document.fullscreenElement) {
shakaUi.getControls()?.toggleFullScreen();
}
return false;
@@ -557,7 +560,7 @@
}
}
if (!document.fullscreenElement && userWantsFullscreen) {
if (!document.fullscreenElement) {
shakaUi.getControls()?.toggleFullScreen();
}
return false;
@@ -577,9 +580,11 @@
playerElement.playbackRate = playerElement.playbackRate - 0.25;
if (!document.fullscreenElement && userWantsFullscreen) {
if (!document.fullscreenElement) {
shakaUi.getControls()?.toggleFullScreen();
}
return false;
});
Mousetrap.bind('shift+right', () => {
@@ -587,13 +592,28 @@
playerElement.playbackRate = playerElement.playbackRate + 0.25;
if (!document.fullscreenElement && userWantsFullscreen) {
if (!document.fullscreenElement) {
shakaUi.getControls()?.toggleFullScreen();
}
return false;
});
setChapterMarkers();
if (await androidTv.isAndroidTv()) {
shakaUi.getControls()?.toggleFullScreen();
Mousetrap.bind('enter', () => {
if (playerElement?.paused) {
playerElement?.play();
} else {
playerElement?.pause();
}
return false;
});
}
try {
await loadVideo();
} catch (error: unknown) {
+17 -3
View File
@@ -9,6 +9,7 @@
interfaceSearchSuggestionsStore,
searchHistoryStore
} from '../store';
import androidTv from '$lib/android/plugins/androidTv';
const dispatch = createEventDispatcher();
@@ -21,6 +22,8 @@
let showSearchBox = $state(false);
let isAndroidTv = $state(false);
let debounceTimer: NodeJS.Timeout;
function debouncedSearch(event: any) {
if (!searchSuggestions) return;
@@ -88,7 +91,8 @@
showSearchBox = false;
}
onMount(() => {
onMount(async () => {
isAndroidTv = await androidTv.isAndroidTv();
Mousetrap.bind(['ctrl+k', 'command+k'], () => {
document.getElementById('search-box')?.focus();
showSearchBox = !showSearchBox;
@@ -105,10 +109,15 @@
await tick();
document.getElementById('search')?.focus();
}}
tabindex="0"
role="presentation"
>
<div class="field prefix fill no-margin search rounded">
<i class="front">search</i>
<div
class="field prefix fill no-margin rounded"
class:search={!isAndroidTv}
class:search-tv={isAndroidTv}
>
<i class="front" tabindex="-1">search</i>
<input
id="search-box"
placeholder={$_('searchPlaceholder')}
@@ -175,6 +184,11 @@
.search {
width: 500px;
}
.search-tv {
width: 600px;
}
.selected {
background-color: var(--surface-variant);
}
@@ -42,7 +42,7 @@
</nav>
<div>
<nav class="tabbed small">
<nav class="tabbed small" tabindex="0" role="region">
<a class:active={activeTab === 'interface'} onclick={() => (activeTab = 'interface')}>
<i>grid_view</i>
<span>{$_('layout.interface')}</span>
@@ -19,6 +19,8 @@
synciousInstanceStore,
synciousStore
} from '../store';
import { goto } from '$app/navigation';
import androidTv from '$lib/android/plugins/androidTv';
interface Props {
video: VideoBase | Video | Notification | PlaylistPageVideo;
@@ -151,9 +153,19 @@
}
</script>
<div class:sideways-root={sideways}>
<div onfocus={() => {}} id="thumbnail-container" role="region">
<div
class:sideways-root={sideways}
tabindex="0"
role="button"
onclick={async () => {
if (await androidTv.isAndroidTv()) {
goto(watchUrl.toString());
}
}}
>
<div id="thumbnail-container">
<a
tabindex="-1"
class="wave thumbnail"
href={watchUrl.toString()}
data-sveltekit-preload-data="off"
@@ -199,6 +211,7 @@
<div class="thumbnail-details video-title">
<div class="video-title">
<a
tabindex="-1"
style="padding-left: 1px;"
class="video-title"
data-sveltekit-preload-data="off"
@@ -208,7 +221,9 @@
</a>
<div>
<a class:author={!sideways} href={`/channel/${video.authorId}`}>{video.author}</a>
<a tabindex="-1" class:author={!sideways} href={`/channel/${video.authorId}`}
>{video.author}</a
>
{#if !('publishedText' in video) && 'viewCountText' in video}
<span style="margin-top: 1em;">
{video.viewCountText}
+5 -2
View File
@@ -11,13 +11,16 @@
}
main.root {
padding-left: 7em !important;
padding-top: 5em !important;
padding-top: 6em !important;
padding-bottom: 5em !important;
max-height: 100vh;
overflow-y: auto;
}
.root-not-tv {
padding-left: 7em !important;
}
.row {
padding: 1em !important;
}
@@ -281,8 +281,8 @@
position: absolute;
background: linear-gradient(
to right,
rgba(255, 255, 255, 0.3) 0%,
rgba(255, 255, 255, 0.3) calc(100% - 5px),
var(--primary-container) 0%,
var(--primary-container) calc(100% - 10px),
transparent 100%
);
}
@@ -4,6 +4,8 @@
"copyUrl": "Copy URL",
"loadMore": "Load more",
"views": "views",
"login": "Login",
"invidiousLogin": "Please log in with your Invidious account",
"invalidInstance": "Please verify the URL. If it's correct, the instance may be down or may not support third-party clients.",
"invidiousBlockWarning": "Invidious is currently being blocked by Google. If videos aren't loading for this instance, please use this instance on Materialious on {android} or {desktop} to get around this with local video fallback.",
"videos": "videos",
+1
View File
@@ -5,6 +5,7 @@ import type Peer from 'peerjs';
import { get } from 'svelte/store';
import { instanceStore, interfaceAllowInsecureRequests } from './store';
import type { Channel, HashTag, Playlist, PlaylistPageVideo, Video, VideoBase } from './api/model';
import { Capacitor } from '@capacitor/core';
export function truncate(value: string, maxLength: number = 50): string {
return value.length > maxLength ? `${value.substring(0, maxLength)}...` : value;
+122 -48
View File
@@ -20,7 +20,6 @@
instanceStore,
interfaceAmoledTheme,
interfaceDefaultPage,
showWarningStore,
syncPartyPeerStore,
themeColorStore
} from '$lib/store';
@@ -35,11 +34,14 @@
import { _ } from '$lib/i18n';
import { get } from 'svelte/store';
import { pwaInfo } from 'virtual:pwa-info';
import androidTv from '$lib/android/plugins/androidTv';
let { children } = $props();
let mobileSearchShow = $state(false);
let isAndroidTv = $state(false);
let isLoggedIn = $state(false);
authStore.subscribe((value) => {
isLoggedIn = value !== null;
@@ -94,21 +96,67 @@
});
async function login() {
const path = new URL(`${get(instanceStore)}/authorize_token`);
const searchParams = new URLSearchParams({
scopes: ':feed,:subscriptions*,:playlists*,:history*,:notifications*'
});
if (Capacitor.getPlatform() === 'android') {
searchParams.set('callback_url', 'materialious-auth://');
path.search = searchParams.toString();
await Browser.open({ url: path.toString() });
if (!isAndroidTv) {
const path = new URL(`${get(instanceStore)}/authorize_token`);
const searchParams = new URLSearchParams({
scopes: ':feed,:subscriptions*,:playlists*,:history*,:notifications*'
});
if (Capacitor.getPlatform() === 'android') {
searchParams.set('callback_url', 'materialious-auth://');
path.search = searchParams.toString();
await Browser.open({ url: path.toString() });
} else {
searchParams.set('callback_url', `${location.origin}/auth`);
path.search = searchParams.toString();
document.location.href = path.toString();
}
} else {
searchParams.set('callback_url', `${location.origin}/auth`);
path.search = searchParams.toString();
document.location.href = path.toString();
await ui('#tv-login');
document.getElementById('username')?.focus();
}
}
let loginError: boolean = $state(false);
let rawUsername: string = $state('');
let rawPassword: string = $state('');
async function usernamePasswordLogin(event: Event) {
event.preventDefault();
loginError = false;
const body = new FormData();
body.append('email', rawUsername);
body.append('password', rawPassword);
body.append('action', 'signin');
const response = await fetch(`${$instanceStore}/login?type=invidious`, {
method: 'POST',
body: body,
headers: {
__redirect: 'manual',
__custom_return: 'json-headers'
}
});
if (response.ok) {
const headers = await response.json();
if ('set-cookie' in headers) {
const sid = (headers['set-cookie'][0].split(';') as string[]).find((cookie) =>
cookie.startsWith('SID=')
);
if (sid) {
console.log(sid);
authStore.set({ username: rawUsername, token: sid });
await ui('#tv-login');
goto('/', { replaceState: true });
return;
}
}
}
loginError = true;
}
function logout() {
authStore.set(null);
goto('/');
@@ -122,6 +170,8 @@
onMount(async () => {
ui();
isAndroidTv = await androidTv.isAndroidTv();
document.addEventListener('click', linkClickOverwrite);
loadSettingsFromEnv();
@@ -172,20 +222,22 @@
{@html webManifestLink}
</svelte:head>
<nav class="left m l small">
<header></header>
{#each getPages() as navPage}
{#if !navPage.requiresAuth || isLoggedIn}
<a
href={navPage.href}
class:active={$page.url.href.endsWith(navPage.href)}
data-sveltekit-preload-data="off"
><i>{navPage.icon}</i>
<div>{navPage.name}</div>
</a>
{/if}
{/each}
</nav>
{#if !isAndroidTv}
<nav class="left m l small">
<header></header>
{#each getPages() as navPage}
{#if !navPage.requiresAuth || isLoggedIn}
<a
href={navPage.href}
class:active={$page.url.href.endsWith(navPage.href)}
data-sveltekit-preload-data="off"
><i>{navPage.icon}</i>
<div>{navPage.name}</div>
</a>
{/if}
{/each}
</nav>
{/if}
<nav class="top">
{#if !mobileSearchShow}
@@ -299,28 +351,23 @@
{/if}
</dialog>
<main class="responsive max root">
{#if Capacitor.getPlatform() === 'web' && $showWarningStore}
<dialog class="active small" style="height: fit-content;">
<h5>Warning</h5>
<div>
<p>
{@html $_('invidiousBlockWarning', {
values: {
android:
'<a href="https://github.com/Materialious/Materialious?tab=readme-ov-file#android" target="_blank" class="link" rel="noopener noreferrer">Android</a>',
desktop:
'<a href="https://github.com/Materialious/Materialious?tab=readme-ov-file#desktop-windowsmacoslinux" target="_blank" class="link" rel="noopener noreferrer">Desktop</a>'
}
})}
</p>
</div>
<nav class="right-align no-space">
<button class="transparent link" onclick={() => showWarningStore.set(false)}
>Don't show again</button
>
</nav>
</dialog>
<main class="responsive max root" tabindex="0" role="region" class:root-not-tv={!isAndroidTv}>
{#if isAndroidTv}
<div class="tabs">
{#each getPages() as navPage}
{#if !navPage.requiresAuth || isLoggedIn}
<a
href={navPage.href}
class:active={$page.url.href.endsWith(navPage.href)}
class="active"
data-sveltekit-preload-data="off"
>
<i>{navPage.icon}</i>
<span>{navPage.name}</span>
</a>
{/if}
{/each}
</div>
{/if}
{#if $navigating}
<PageLoading />
@@ -331,6 +378,33 @@
<SyncParty />
</main>
<dialog class="modal" id="tv-login">
<h5>{$_('loginRequired')}</h5>
<div>{$_('invidiousLogin')}</div>
<form onsubmit={usernamePasswordLogin}>
<div class="field label border" class:invalid={loginError}>
<input id="username" bind:value={rawUsername} name="username" type="text" />
<label for="username">Username</label>
</div>
<div class="field label border" class:invalid={loginError}>
<input bind:value={rawPassword} name="password" type="password" />
<label for="password">Password</label>
</div>
<nav class="right-align no-space">
<button
class="transparent link"
type="button"
onclick={async () => {
await ui('#tv-login');
}}>{$_('cancel')}</button
>
<button class="transparent link" type="submit">{$_('login')}</button>
</nav>
</form>
</dialog>
<style>
nav.left a {
font-size: 0.8em;
+40 -3
View File
@@ -26,7 +26,10 @@ const CORS_HEADERS = [
'X-Goog-FieldMask',
'Range',
'Referer',
'Cookie'
'Cookie',
'__redirect',
'__custom_return',
'__sid_auth'
].join(', ');
const CORS_ORIGIN = 'https://www.youtube.com';
@@ -47,6 +50,11 @@ function proxyRequest(clientReq, clientRes, parsedUrl, bodyChunks = [], redirect
return clientRes.end('Too many redirects');
}
const redirectAllowed = clientReq.headers.__redirect !== 'manual';
delete clientReq.headers.__redirect;
const returnHeadersAsJson = clientReq.headers.__custom_return === 'json-headers';
delete clientReq.headers.__custom_return;
const isHttps = parsedUrl.protocol === 'https:';
const httpClient = isHttps ? https : http;
@@ -62,6 +70,11 @@ function proxyRequest(clientReq, clientRes, parsedUrl, bodyChunks = [], redirect
rejectUnauthorized: rejectUnauthorized
};
if (clientReq.headers.__sid_auth) {
proxyOptions.headers.cookie = clientReq.headers.__sid_auth;
delete proxyOptions.headers.__sid_auth;
}
// Remove headers that may cause issues or be auto-managed
for (const key of [
'referer',
@@ -75,13 +88,37 @@ function proxyRequest(clientReq, clientRes, parsedUrl, bodyChunks = [], redirect
}
const proxyReq = httpClient.request(parsedUrl, proxyOptions, (proxyRes) => {
// Handle redirects
if (proxyRes.statusCode >= 300 && proxyRes.statusCode < 400 && proxyRes.headers.location) {
// Handle redirects normally if redirectAllowed
if (
proxyRes.statusCode >= 300 &&
proxyRes.statusCode < 400 &&
proxyRes.headers.location &&
redirectAllowed
) {
proxyRes.resume(); // discard response data
const newUrl = new URL(proxyRes.headers.location, parsedUrl);
return proxyRequest(clientReq, clientRes, newUrl, bodyChunks, redirectCount + 1);
}
if (returnHeadersAsJson) {
const headersDict = {};
for (const [key, value] of Object.entries(proxyRes.headers)) {
headersDict[key] = value;
}
const body = JSON.stringify(headersDict);
clientRes.writeHead(200, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': CORS_ORIGIN,
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': CORS_HEADERS,
'Access-Control-Allow-Credentials': 'true',
'Content-Length': Buffer.byteLength(body)
});
return clientRes.end(body);
}
clientRes.writeHead(proxyRes.statusCode, {
...proxyRes.headers,
'Access-Control-Allow-Origin': CORS_ORIGIN,
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

+1 -1
View File
@@ -3,7 +3,7 @@ import os
import re
from datetime import datetime
LATEST_VERSION = "1.9.8"
LATEST_VERSION = "1.9.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")