@@ -43,6 +43,13 @@
|
||||
- PWA support.
|
||||
- YT path redirects (So your redirect plugins should still work!)
|
||||
|
||||
# Support table
|
||||
| | Dash | HLS | Local video fallback | API-Extended | ffmpeg download merging | Dearrow | RYD |
|
||||
|---------|------|-----|----------------------|--------------|-------------------------|---------|-----|
|
||||
| Web | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Desktop | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
|
||||
| Android | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
|
||||
|
||||
# Public instances
|
||||
[Public instances are listed here](./docs/INSTANCES.md).
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ android {
|
||||
applicationId "us.materialio.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 40
|
||||
versionName "1.4.3"
|
||||
versionCode 41
|
||||
versionName "1.5.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -11,6 +11,8 @@ apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
implementation project(':capacitor-app')
|
||||
implementation project(':capacitor-browser')
|
||||
implementation project(':capgo-inappbrowser')
|
||||
implementation project(':capacitor-nodejs')
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
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"
|
||||
|
||||
@@ -7,3 +7,9 @@ project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/
|
||||
|
||||
include ':capacitor-browser'
|
||||
project(':capacitor-browser').projectDir = new File('../node_modules/@capacitor/browser/android')
|
||||
|
||||
include ':capgo-inappbrowser'
|
||||
project(':capgo-inappbrowser').projectDir = new File('../node_modules/@capgo/inappbrowser/android')
|
||||
|
||||
include ':capacitor-nodejs'
|
||||
project(':capacitor-nodejs').projectDir = new File('../node_modules/capacitor-nodejs/android')
|
||||
|
||||
@@ -4,10 +4,9 @@ const config: CapacitorConfig = {
|
||||
appId: 'us.materialio.app',
|
||||
appName: 'Materialious',
|
||||
webDir: 'build',
|
||||
bundledWebRuntime: false,
|
||||
plugins: {
|
||||
CapacitorHttp: {
|
||||
enabled: true
|
||||
CapacitorNodeJS: {
|
||||
nodeDir: 'nodejs-android'
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,10 +4,9 @@ const config: CapacitorConfig = {
|
||||
appId: 'us.materialio.app',
|
||||
appName: 'Materialious',
|
||||
webDir: 'build',
|
||||
bundledWebRuntime: false,
|
||||
plugins: {
|
||||
CapacitorHttp: {
|
||||
enabled: true
|
||||
CapacitorNodeJS: {
|
||||
nodeDir: 'nodejs-android'
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.4.3",
|
||||
"version": "1.5.0",
|
||||
"description": "Modern material design for Invidious.",
|
||||
"author": {
|
||||
"name": "Ward Pearce",
|
||||
@@ -21,6 +21,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.0",
|
||||
"capacitor-nodejs": "https://github.com/EdenwareApps/Capacitor-NodeJS/releases/download/v1.0.0-beta.7/capacitor6-nodejs.tgz",
|
||||
"chokidar": "~3.6.0",
|
||||
"electron-is-dev": "~2.0.0",
|
||||
"electron-serve": "~1.1.0",
|
||||
@@ -40,4 +41,4 @@
|
||||
"capacitor",
|
||||
"electron"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const CapacitorNodejs = require('../../../node_modules/capacitor-nodejs/electron/dist/plugin.js');
|
||||
|
||||
module.exports = {
|
||||
CapacitorNodejs,
|
||||
}
|
||||
Generated
+1033
-467
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.4.3",
|
||||
"version": "1.5.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
@@ -31,6 +31,7 @@
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.44.0",
|
||||
"i": "^0.3.7",
|
||||
"jsdom": "^25.0.0",
|
||||
"npm": "^10.8.3",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
@@ -49,8 +50,11 @@
|
||||
"@capacitor/browser": "^6.0.2",
|
||||
"@capacitor/cli": "6.1.2",
|
||||
"@capacitor/core": "^6.1.2",
|
||||
"@capgo/inappbrowser": "^6.6.8",
|
||||
"@ffmpeg/ffmpeg": "^0.12.10",
|
||||
"beercss": "^3.7.7",
|
||||
"capacitor-nodejs": "https://github.com/EdenwareApps/Capacitor-NodeJS/releases/download/v1.0.0-beta.7/capacitor6-nodejs.tgz",
|
||||
"dashjs": "^4.7.4",
|
||||
"fuse.js": "^7.0.0",
|
||||
"he": "^1.2.0",
|
||||
"human-number": "^2.0.4",
|
||||
@@ -64,6 +68,7 @@
|
||||
"svelte-i18n": "^4.0.0",
|
||||
"svelte-infinite-loading": "^1.4.0",
|
||||
"svelte-persisted-store": "^0.11.0",
|
||||
"terser": "^5.33.0",
|
||||
"vidstack": "^1.12.9"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ export async function getPopular(): Promise<Video[]> {
|
||||
|
||||
export async function getVideo(videoId: string, local: boolean = false): Promise<VideoPlay> {
|
||||
const resp = await fetch(setRegion(buildPath(`videos/${videoId}?local=${local}`)));
|
||||
if (!resp.ok && Capacitor.getPlatform() === 'electron' && get(playerYouTubeJsFallback)) {
|
||||
if (!resp.ok && Capacitor.isNativePlatform() && get(playerYouTubeJsFallback)) {
|
||||
return await patchYoutubeJs(videoId);
|
||||
} else {
|
||||
await fetchErrorHandle(resp);
|
||||
|
||||
@@ -102,6 +102,7 @@ export interface VideoPlay extends Video {
|
||||
storyboardHeight: number;
|
||||
storyboardCount: number;
|
||||
}[];
|
||||
fallbackPatch?: 'youtubejs' | 'piped';
|
||||
}
|
||||
|
||||
export interface ReturnYTDislikes {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $miniPlayerSrcStore}
|
||||
{#if $miniPlayerSrcStore && $miniPlayerSrcStore.video.formatStreams.length > 0}
|
||||
<nav class="bottom no-padding">
|
||||
<article class="surface-bright">
|
||||
<button on:click={() => miniPlayerSrcStore.set(null)} class="s circle transparent no-margin">
|
||||
|
||||
@@ -8,13 +8,12 @@
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import type { MediaTimeUpdateEvent, PlayerSrc, VideoSrc } from 'vidstack';
|
||||
import type { MediaTimeUpdateEvent, PlayerSrc } from 'vidstack';
|
||||
import type { MediaPlayerElement } from 'vidstack/elements';
|
||||
import { deleteVideoProgress, getVideoProgress, saveVideoProgress } from './Api';
|
||||
import type { VideoPlay } from './Api/model';
|
||||
import {
|
||||
getBestThumbnail,
|
||||
proxyVideoUrl,
|
||||
pullBitratePreference,
|
||||
videoLength,
|
||||
type PhasedDescription
|
||||
@@ -25,7 +24,6 @@
|
||||
miniPlayerSrcStore,
|
||||
playerAlwaysLoopStore,
|
||||
playerAutoPlayStore,
|
||||
playerDashStore,
|
||||
playerProxyVideosStore,
|
||||
playerSavePlaybackPositionStore,
|
||||
silenceSkipperStore,
|
||||
@@ -47,7 +45,7 @@
|
||||
export let segments: Segment[] = [];
|
||||
|
||||
let src: PlayerSrc = [];
|
||||
let categoryBeingSkipped = '';
|
||||
let snackBarAlert = '';
|
||||
let playerIsLive = false;
|
||||
let playerPosSet = false;
|
||||
|
||||
@@ -261,10 +259,11 @@
|
||||
});
|
||||
|
||||
player.addEventListener('provider-change', (event) => {
|
||||
const provider = event.detail;
|
||||
const provider = event.detail as any;
|
||||
const bitrate = pullBitratePreference();
|
||||
if (provider?.type === 'dash') {
|
||||
(provider as any).config = {
|
||||
provider.library = () => import('dashjs');
|
||||
provider.config = {
|
||||
streaming: {
|
||||
abr: {
|
||||
ABRStrategy: 'abrBola',
|
||||
@@ -312,9 +311,11 @@
|
||||
if (Math.round(player.currentTime) >= Math.round(player.duration)) {
|
||||
return;
|
||||
}
|
||||
categoryBeingSkipped = segment.category;
|
||||
player.currentTime = segment.endTime + 1;
|
||||
ui('#sponsorblock-alert');
|
||||
if (!get(sponsorBlockDisplayToastStore)) {
|
||||
snackBarAlert = `${get(_)('skipping')} ${segment.category}`;
|
||||
ui('#snackbar-alert');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -322,38 +323,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (get(playerDashStore)) {
|
||||
src = [{ src: data.video.dashUrl, type: 'application/dash+xml' }];
|
||||
src = [{ src: data.video.dashUrl, type: 'application/dash+xml' }];
|
||||
|
||||
if (!data.video.fallbackPatch) {
|
||||
if (Capacitor.getPlatform() !== 'electron' || proxyVideos) {
|
||||
(src[0] as { src: string }).src += '?local=true';
|
||||
}
|
||||
|
||||
player.addEventListener('dash-can-play', async () => {
|
||||
if (get(playerAutoPlayStore)) {
|
||||
player.play();
|
||||
}
|
||||
await loadPlayerPos();
|
||||
});
|
||||
} else {
|
||||
let formattedSrc;
|
||||
src = data.video.formatStreams.map((format) => {
|
||||
if (proxyVideos) {
|
||||
formattedSrc = proxyVideoUrl(format.url);
|
||||
} else {
|
||||
formattedSrc = format.url;
|
||||
}
|
||||
const quality = format.size.split('x');
|
||||
return {
|
||||
src: formattedSrc,
|
||||
type: format.type.split(';')[0],
|
||||
height: Number(quality[1]),
|
||||
width: Number(quality[0])
|
||||
};
|
||||
}) as VideoSrc[];
|
||||
|
||||
await loadPlayerPos();
|
||||
}
|
||||
|
||||
player.addEventListener('dash-can-play', async () => {
|
||||
if (get(playerAutoPlayStore)) {
|
||||
player.play();
|
||||
}
|
||||
await loadPlayerPos();
|
||||
});
|
||||
} else {
|
||||
playerIsLive = true;
|
||||
src = [
|
||||
@@ -368,6 +351,11 @@
|
||||
|
||||
const currentTheme = await getDynamicTheme();
|
||||
|
||||
if (data.video.fallbackPatch === 'youtubejs') {
|
||||
snackBarAlert = get(_)('player.youtubeJsFallBack');
|
||||
ui('#snackbar-alert');
|
||||
}
|
||||
|
||||
document.documentElement.style.setProperty(
|
||||
'--media-slider-track-fill-bg',
|
||||
currentTheme['--primary']
|
||||
@@ -488,11 +476,8 @@
|
||||
{/if}
|
||||
</media-player>
|
||||
|
||||
{#if !isEmbed && !$sponsorBlockDisplayToastStore}
|
||||
<div class="snackbar" id="sponsorblock-alert">
|
||||
<span
|
||||
>{$_('skipping')}
|
||||
<span class="bold" style="text-transform: capitalize;">{categoryBeingSkipped}</span></span
|
||||
>
|
||||
{#if !isEmbed}
|
||||
<div class="snackbar" id="snackbar-alert">
|
||||
<span class="bold" style="text-transform: capitalize;">{snackBarAlert}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { bookmarkletSaveToUrl } from '$lib/externalSettings';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import ui from 'beercss';
|
||||
@@ -21,10 +22,8 @@
|
||||
interfaceRegionStore,
|
||||
interfaceSearchSuggestionsStore,
|
||||
playerAlwaysLoopStore,
|
||||
playerAndroidBackgroundPlayStore,
|
||||
playerAutoPlayStore,
|
||||
playerAutoplayNextByDefaultStore,
|
||||
playerDashStore,
|
||||
playerListenByDefaultStore,
|
||||
playerMiniPlayerStore,
|
||||
playerProxyVideosStore,
|
||||
@@ -125,8 +124,10 @@
|
||||
<h6>Invidious</h6>
|
||||
<form
|
||||
on:submit|preventDefault={() => {
|
||||
authStore.set(null);
|
||||
instanceStore.set(ensureNoTrailingSlash(invidiousInstance));
|
||||
authStore.set(null);
|
||||
goto('/', { replaceState: true });
|
||||
ui('#dialog-settings');
|
||||
}}
|
||||
>
|
||||
<nav>
|
||||
@@ -305,7 +306,7 @@
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{#if Capacitor.getPlatform() !== 'android'}
|
||||
{#if Capacitor.isNativePlatform()}
|
||||
<div class="field no-margin">
|
||||
<nav class="no-padding">
|
||||
<div class="max">
|
||||
@@ -388,43 +389,6 @@
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{#if Capacitor.getPlatform() === 'android'}
|
||||
<div class="field no-margin">
|
||||
<nav class="no-padding">
|
||||
<div class="max">
|
||||
<div>{$_('layout.player.backgroundPlay')}</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={$playerAndroidBackgroundPlayStore}
|
||||
on:click={() =>
|
||||
playerAndroidBackgroundPlayStore.set(!$playerAndroidBackgroundPlayStore)}
|
||||
/>
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if Capacitor.getPlatform() !== 'android'}
|
||||
<div class="field no-margin">
|
||||
<nav class="no-padding">
|
||||
<div class="max">
|
||||
<div>{$_('layout.player.dash')}</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={$playerDashStore}
|
||||
on:click={() => playerDashStore.set(!$playerDashStore)}
|
||||
/>
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="field no-margin">
|
||||
<nav class="no-padding">
|
||||
<div class="max">
|
||||
@@ -441,7 +405,7 @@
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{#if Capacitor.getPlatform() === 'electron'}
|
||||
{#if Capacitor.isNativePlatform()}
|
||||
<div class="field no-margin">
|
||||
<nav class="no-padding">
|
||||
<div class="max">
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
playerAlwaysLoopStore,
|
||||
playerAutoPlayStore,
|
||||
playerAutoplayNextByDefaultStore,
|
||||
playerDashStore,
|
||||
playerListenByDefaultStore,
|
||||
playerMiniPlayerStore,
|
||||
playerProxyVideosStore,
|
||||
@@ -74,11 +73,6 @@ const persistedStores = [
|
||||
store: playerSavePlaybackPositionStore,
|
||||
type: 'boolean'
|
||||
},
|
||||
{
|
||||
name: 'dashEnabled',
|
||||
store: playerDashStore,
|
||||
type: 'boolean'
|
||||
},
|
||||
{
|
||||
name: 'theatreModeByDefault',
|
||||
store: playerTheatreModeByDefaultStore,
|
||||
|
||||
@@ -82,7 +82,8 @@
|
||||
},
|
||||
"noPlaylists": "No playlists",
|
||||
"unableToLoadComments": "Unable to load comments",
|
||||
"addToPlaylist": "Add to playlist"
|
||||
"addToPlaylist": "Add to playlist",
|
||||
"youtubeJsFallBack": "Local video fallback being used"
|
||||
},
|
||||
"layout": {
|
||||
"interface": "Interface",
|
||||
@@ -124,7 +125,7 @@
|
||||
"backgroundPlay": "Play audio in background",
|
||||
"dash": "Dash",
|
||||
"silenceSkipper": "Silence skipper (Experimental)",
|
||||
"youtubeJsFallback": "Locally video processing fallback"
|
||||
"youtubeJsFallback": "Local video processing fallback"
|
||||
},
|
||||
"bookmarklet": "Bookmarklet",
|
||||
"instanceUrl": "Instance URL",
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { goto } from "$app/navigation";
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
|
||||
if (Capacitor.getPlatform() === 'android') {
|
||||
const originalFetch = window.fetch;
|
||||
|
||||
// CORS-Anywhere proxy URL
|
||||
const corsAnywhereProxyUrl: string = 'http://localhost:3000/';
|
||||
|
||||
// Overwrite fetch to use CORS-Anywhere proxy
|
||||
window.fetch = async (requestInput: RequestInfo | URL, requestOptions: RequestInit = {}): Promise<Response> => {
|
||||
const requestUrl: string =
|
||||
typeof requestInput === 'string' ? requestInput : requestInput.toString();
|
||||
|
||||
// Check if the URL is already proxied, to avoid double proxying
|
||||
if (!requestUrl.startsWith(corsAnywhereProxyUrl)) {
|
||||
requestInput = corsAnywhereProxyUrl + requestUrl;
|
||||
}
|
||||
|
||||
// Ensure options.method is set to a valid value
|
||||
requestOptions.method = requestOptions.method ? requestOptions.method.toUpperCase() : 'GET';
|
||||
|
||||
// Use the original fetch with the proxied URL and options
|
||||
return originalFetch(requestInput, requestOptions);
|
||||
};
|
||||
|
||||
const currentOrigin: string = window.location.protocol + '//' + window.location.host;
|
||||
const originalXhrOpen = XMLHttpRequest.prototype.open;
|
||||
|
||||
XMLHttpRequest.prototype.open = function (...args: any[]): void {
|
||||
const targetOriginMatch = /^https?:\/\/([^\/]+)/i.exec(args[1]);
|
||||
if (targetOriginMatch && targetOriginMatch[0].toLowerCase() !== currentOrigin) {
|
||||
args[1] = corsAnywhereProxyUrl + args[1];
|
||||
}
|
||||
/* @ts-ignore */
|
||||
return originalXhrOpen.apply(this, args);
|
||||
};
|
||||
|
||||
// Must reload page after patches
|
||||
goto('/', { replaceState: true });
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { InAppBrowser } from '@capgo/inappbrowser';
|
||||
import { minify } from 'terser';
|
||||
|
||||
export interface PoTokens {
|
||||
visitor_data: string;
|
||||
po_token: string;
|
||||
}
|
||||
|
||||
const headers = {
|
||||
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||||
'accept-language': 'en-US;q=0.9',
|
||||
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko)',
|
||||
};
|
||||
|
||||
// Inspired by https://github.com/YunzheZJU/youtube-po-token-generator & https://github.com/YunzheZJU/youtube-po-token-generator
|
||||
export async function getPoToken(): Promise<PoTokens> {
|
||||
let visitorData: string;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const messageListener = async (data: any) => {
|
||||
|
||||
if (!data.po_token) {
|
||||
visitorData = data.visitor_data;
|
||||
const injectPlayer = await fetch('/po-token-youtube/inject.js');
|
||||
|
||||
await InAppBrowser.executeScript({
|
||||
code: (await minify(`
|
||||
window.onPoToken = (poToken) => {
|
||||
window.mobileApp.postMessage({po_token: poToken});
|
||||
}
|
||||
|
||||
ytcfg.set({
|
||||
'INNERTUBE_CONTEXT': {
|
||||
...ytcfg.get('INNERTUBE_CONTEXT'),
|
||||
client: {
|
||||
...ytcfg.get('INNERTUBE_CONTEXT').client,
|
||||
visitorData: "${visitorData}",
|
||||
},
|
||||
},
|
||||
'VISITOR_DATA': "${visitorData}",
|
||||
'IDENTITY_MEMENTO': {
|
||||
'visitor_data': "${visitorData}",
|
||||
},
|
||||
})
|
||||
|
||||
${await injectPlayer.text()}`)).code as string
|
||||
});
|
||||
} else if (visitorData && data.po_token) {
|
||||
await InAppBrowser.removeAllListeners();
|
||||
await InAppBrowser.close();
|
||||
resolve({ visitor_data: visitorData, po_token: data.po_token });
|
||||
}
|
||||
};
|
||||
|
||||
const closeListener = async () => {
|
||||
await InAppBrowser.removeAllListeners();
|
||||
};
|
||||
|
||||
const urlChangeListener = async () => {
|
||||
// Code must be minified to ensure runs correctly.
|
||||
await InAppBrowser.executeScript({
|
||||
code: (await minify(`const headers = {"accept": "${headers.accept}", "accept-language": "${headers['accept-language']}", "user-agent": "${headers['user-agent']}"};
|
||||
Object.defineProperty(window.navigator, 'userAgent', { value: headers["user-agent"], writable: false });
|
||||
|
||||
const originalOpen = XMLHttpRequest.prototype.open;
|
||||
const originalSend = XMLHttpRequest.prototype.send;
|
||||
|
||||
XMLHttpRequest.prototype.open = function (method, url) {
|
||||
this._url = url;
|
||||
originalOpen.apply(this, arguments);
|
||||
};
|
||||
|
||||
XMLHttpRequest.prototype.send = function (body) {
|
||||
const xhr = this;
|
||||
if (this._url.includes("/youtubei/v1/player")) {
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
this.setRequestHeader(key, value);
|
||||
}
|
||||
|
||||
this.addEventListener("load", function () {
|
||||
try {
|
||||
const postJson = JSON.parse(body);
|
||||
window.mobileApp.postMessage({visitor_data: postJson.context.client.visitorData});
|
||||
} catch (error) {
|
||||
console.error("Error parsing request body:", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
originalSend.apply(this, arguments);
|
||||
};
|
||||
|
||||
function attemptClickPlayButton() {
|
||||
const interval = setInterval(() => {
|
||||
const playButtons = document.getElementsByClassName("ytp-play-button");
|
||||
if (playButtons.length > 0) {
|
||||
playButtons[0].click();
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
attemptClickPlayButton();
|
||||
`)).code as string
|
||||
});
|
||||
};
|
||||
|
||||
InAppBrowser.addListener('messageFromWebview', messageListener);
|
||||
InAppBrowser.addListener('closeEvent', closeListener);
|
||||
InAppBrowser.addListener('urlChangeEvent', urlChangeListener);
|
||||
|
||||
setTimeout(() => {
|
||||
reject(new Error('Timeout trying to pull Po tokens'));
|
||||
InAppBrowser.removeAllListeners();
|
||||
}, 30000);
|
||||
|
||||
InAppBrowser.openWebView({
|
||||
url: 'https://www.youtube.com/embed/jNQXAC9IVRw',
|
||||
title: 'Pulling po tokens',
|
||||
headers: headers
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,14 +1,33 @@
|
||||
import type { Image, Thumbnail, VideoBase, VideoPlay } from '$lib/Api/model';
|
||||
import { numberWithCommas } from '$lib/misc';
|
||||
import { poTokenCacheStore } from '$lib/store';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { get } from 'svelte/store';
|
||||
import { type PoTokens } from './poTokenAndroid';
|
||||
|
||||
export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
|
||||
const innertube = (await import('youtubei.js')).Innertube;
|
||||
|
||||
const tokens = await (window as any).electron.generatePoToken();
|
||||
let tokens: PoTokens;
|
||||
|
||||
const poTokenCache = get(poTokenCacheStore);
|
||||
if (!poTokenCache) {
|
||||
if (Capacitor.getPlatform() === 'electron') {
|
||||
tokens = await (window as any).electron.generatePoToken();
|
||||
} else if (Capacitor.getPlatform() === 'android') {
|
||||
tokens = await (await import('../../lib/patches/poTokenAndroid')).getPoToken();
|
||||
} else {
|
||||
throw new Error('This platform cant generate po tokens');
|
||||
}
|
||||
|
||||
poTokenCacheStore.set(tokens);
|
||||
} else {
|
||||
tokens = poTokenCache;
|
||||
}
|
||||
|
||||
const youtube = await innertube.create({
|
||||
visitor_data: tokens.visitorData,
|
||||
po_token: tokens.poToken
|
||||
visitor_data: tokens.visitor_data,
|
||||
po_token: tokens.po_token,
|
||||
});
|
||||
|
||||
const video = await youtube.getInfo(videoId);
|
||||
@@ -52,9 +71,9 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
|
||||
|
||||
return {
|
||||
type: 'video',
|
||||
title: video.primary_info.title.toString(),
|
||||
viewCount: Number(video.primary_info.view_count.toString().replace(/\D/g, '')),
|
||||
viewCountText: video.primary_info.view_count.toString(),
|
||||
title: video.primary_info.title ? video.primary_info.title.toString() : '',
|
||||
viewCount: video.primary_info.view_count ? Number(video.primary_info.view_count.toString().replace(/\D/g, '')) : 0,
|
||||
viewCountText: video.primary_info.view_count ? video.primary_info.view_count.toString() : '',
|
||||
likeCount: video.basic_info.like_count || 0,
|
||||
dislikeCount: 0,
|
||||
allowRatings: false,
|
||||
@@ -75,7 +94,7 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
|
||||
description: descString,
|
||||
descriptionHtml: video.secondary_info.description.toHTML() || descString,
|
||||
published: 0,
|
||||
publishedText: video.primary_info.published.toString(),
|
||||
publishedText: video.primary_info.published ? video.primary_info.published.toString() : '',
|
||||
premiereTimestamp: 0,
|
||||
hlsUrl: video.streaming_data?.hls_manifest_url || undefined,
|
||||
liveNow: video.basic_info.is_live || false,
|
||||
@@ -87,6 +106,7 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
|
||||
lengthSeconds: video.basic_info.duration || 0,
|
||||
subCountText: '',
|
||||
keywords: video.basic_info.keywords || [],
|
||||
allowedRegions: []
|
||||
allowedRegions: [],
|
||||
fallbackPatch: 'youtubejs'
|
||||
};
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import type { DataConnection } from 'peerjs';
|
||||
import { persisted } from 'svelte-persisted-store';
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
import type { TitleCase } from './misc';
|
||||
import type { PoTokens } from './patches/poTokenAndroid';
|
||||
|
||||
function platformDependentDefault(givenValue: any, defaultValue: any): any {
|
||||
if (typeof givenValue !== 'undefined' && typeof givenValue !== null) {
|
||||
@@ -31,14 +32,12 @@ export const activePageStore: Writable<string | null> = writable('home');
|
||||
|
||||
export const playerAutoPlayStore = persisted('autoPlay', true);
|
||||
export const playerAlwaysLoopStore = persisted('alwaysLoop', false);
|
||||
export const playerProxyVideosStore = persisted('proxyVideos', false);
|
||||
export const playerProxyVideosStore = persisted('proxyVideos', true);
|
||||
export const playerListenByDefaultStore = persisted('listenByDefault', false);
|
||||
export const playerSavePlaybackPositionStore = persisted('savePlaybackPosition', true);
|
||||
export const playerDashStore = persisted('dashEnabled', false);
|
||||
export const playerTheatreModeByDefaultStore = persisted('theatreModeByDefault', false);
|
||||
export const playerAutoplayNextByDefaultStore = persisted('autoplayNextByDefault', false);
|
||||
export const playerMiniPlayerStore = persisted('miniPlayer', true);
|
||||
export const playerAndroidBackgroundPlayStore = persisted('androidBackgroundPlayer', true);
|
||||
export const playerYouTubeJsFallback = persisted('youTubeJsFallback', true);
|
||||
|
||||
export const returnYtDislikesStore = persisted('returnYtDislikes', false);
|
||||
@@ -99,3 +98,5 @@ export const miniPlayerSrcStore: Writable<{ video: VideoPlay; time: number; } |
|
||||
writable(null);
|
||||
|
||||
export const silenceSkipperStore: Writable<boolean> = persisted('silenceSkipper', false);
|
||||
|
||||
export const poTokenCacheStore: Writable<PoTokens> = writable();
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import SyncParty from '$lib/SyncParty.svelte';
|
||||
import Thumbnail from '$lib/Thumbnail.svelte';
|
||||
import { bookmarkletLoadFromUrl, loadSettingsFromEnv } from '$lib/externalSettings';
|
||||
import '$lib/patches/androidRequests';
|
||||
import {
|
||||
activePageStore,
|
||||
authStore,
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
import { phaseDescription } from '$lib/misc';
|
||||
import {
|
||||
authStore,
|
||||
playerDashStore,
|
||||
playerProxyVideosStore,
|
||||
returnYTDislikesInstanceStore,
|
||||
returnYtDislikesStore
|
||||
@@ -56,7 +55,7 @@ export async function load({ params, url }) {
|
||||
if (
|
||||
import.meta.env.VITE_DEFAULT_DOWNLOAD_ENABLED &&
|
||||
import.meta.env.VITE_DEFAULT_DOWNLOAD_ENABLED.toString().toLowerCase() === 'true' &&
|
||||
!video.liveNow && get(playerDashStore)
|
||||
!video.liveNow
|
||||
) {
|
||||
downloadQualitiesDash = listCombinedQualities(video.dashUrl);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
const corsAnywhere = require('cors-anywhere');
|
||||
|
||||
corsAnywhere.createServer({
|
||||
originWhitelist: [],
|
||||
}).listen(3000, 'localhost');
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "cors-patch",
|
||||
"version": "0.0.1",
|
||||
"main": "./index.js",
|
||||
"dependencies": {
|
||||
"cors-anywhere": "^0.4.4"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,9 @@ export default defineConfig({
|
||||
plugins: [
|
||||
SvelteKitPWA({
|
||||
injectRegister: 'inline',
|
||||
workbox: {
|
||||
maximumFileSizeToCacheInBytes: 4000000
|
||||
},
|
||||
manifest: {
|
||||
description: 'Modern material design for Invidious.',
|
||||
theme_color: '#8936FF',
|
||||
@@ -36,5 +39,5 @@ export default defineConfig({
|
||||
],
|
||||
optimizeDeps: {
|
||||
exclude: ['@ffmpeg/ffmpeg', '@ffmpeg/util']
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import json
|
||||
import os
|
||||
import re
|
||||
|
||||
LATEST_VERSION = "1.4.3"
|
||||
LATEST_VERSION = "1.5.0"
|
||||
WORKING_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "materialious")
|
||||
|
||||
ROOT_PACKAGE = os.path.join(WORKING_DIR, "package.json")
|
||||
|
||||
Reference in New Issue
Block a user