From d19b67701efd3e661f3fd9b70a3fe1cdd8f77aec Mon Sep 17 00:00:00 2001 From: WardPearce Date: Sun, 22 Sep 2024 01:34:55 +1200 Subject: [PATCH 1/4] 1.5.5 --- .../src/lib/patches/androidRequests.ts | 37 ++++++++++++++----- update_versions.py | 2 +- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/materialious/src/lib/patches/androidRequests.ts b/materialious/src/lib/patches/androidRequests.ts index 30e18f44..5d7a9a91 100644 --- a/materialious/src/lib/patches/androidRequests.ts +++ b/materialious/src/lib/patches/androidRequests.ts @@ -1,6 +1,6 @@ import { goto } from "$app/navigation"; import { Capacitor } from "@capacitor/core"; - +import { NodeJS } from 'capacitor-nodejs'; if (Capacitor.getPlatform() === 'android') { const originalFetch = window.fetch; @@ -9,17 +9,32 @@ if (Capacitor.getPlatform() === 'android') { const corsAnywhereProxyUrl: string = 'http://localhost:3000/'; // Overwrite fetch to use CORS-Anywhere proxy - window.fetch = async (requestInput: RequestInfo | URL, requestOptions: RequestInit = {}): Promise => { - const requestUrl: string = - typeof requestInput === 'string' ? requestInput : requestInput.toString(); + window.fetch = async (requestInput: string | URL | Request, requestOptions?: RequestInit): Promise => { + let requestUrl: string; - // Check if the URL is already proxied, to avoid double proxying - if (!requestUrl.startsWith(corsAnywhereProxyUrl) && !requestUrl.startsWith('/') && !requestUrl.startsWith('blob:') && !requestUrl.startsWith('/')) { + if (typeof requestInput === 'string') { + requestUrl = requestInput; + } else if ('url' in requestInput) { + requestUrl = requestInput.url; + requestOptions = { + body: requestInput.body, + cache: requestInput.cache, + credentials: requestInput.credentials, + headers: requestInput.headers, + method: requestInput.method, + mode: requestInput.mode, + redirect: requestInput.redirect, + referrer: requestInput.referrer, + signal: requestInput.signal, + }; + } else { + requestUrl = requestInput.toString(); + } + + if (!requestUrl.startsWith(corsAnywhereProxyUrl) && !requestUrl.startsWith('/') && !requestUrl.startsWith('blob:')) { 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); @@ -38,5 +53,7 @@ if (Capacitor.getPlatform() === 'android') { }; // Must reload page after patches - goto('/', { replaceState: true }); -} \ No newline at end of file + NodeJS.whenReady().then(() => { + goto('/', { replaceState: true }); + }); +} diff --git a/update_versions.py b/update_versions.py index d802a161..40b6dce3 100644 --- a/update_versions.py +++ b/update_versions.py @@ -5,7 +5,7 @@ import json import os import re -LATEST_VERSION = "1.5.4" +LATEST_VERSION = "1.5.5" WORKING_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "materialious") ROOT_PACKAGE = os.path.join(WORKING_DIR, "package.json") From 7c0998e549b3965457e0b0eb8c5d84e19a6744a2 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Sun, 22 Sep 2024 01:38:37 +1200 Subject: [PATCH 2/4] Improve android bg play logic --- materialious/src/lib/Player.svelte | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/materialious/src/lib/Player.svelte b/materialious/src/lib/Player.svelte index 416c0f5a..0cadded3 100644 --- a/materialious/src/lib/Player.svelte +++ b/materialious/src/lib/Player.svelte @@ -350,6 +350,8 @@ const audioId = { audioId: data.video.videoId }; + let isPlayingInBackground = false; + await AudioPlayer.create({ ...audioId, audioSource: proxyVideoUrl(highestBitrateAudio.url), @@ -360,6 +362,10 @@ }); AudioPlayer.onAppGainsFocus(audioId, async () => { + if (!isPlayingInBackground) return; + + isPlayingInBackground = false; + await AudioPlayer.pause(audioId); const audioPlayerTime = await AudioPlayer.getCurrentTime(audioId); @@ -369,13 +375,14 @@ }); AudioPlayer.onAppLosesFocus(audioId, async () => { - if (!player.paused) { - await AudioPlayer.play(audioId); - await AudioPlayer.seek({ - ...audioId, - timeInSeconds: Math.round(player.currentTime) - }); - } + if (player.paused) return; + + isPlayingInBackground = true; + await AudioPlayer.play(audioId); + await AudioPlayer.seek({ + ...audioId, + timeInSeconds: Math.round(player.currentTime) + }); }); await AudioPlayer.initialize(audioId); From 1346d5b34917f20e5066490cbd6a7082b5c1cb9b Mon Sep 17 00:00:00 2001 From: WardPearce Date: Sun, 22 Sep 2024 02:07:26 +1200 Subject: [PATCH 3/4] Fix youtubejs fetching --- .../src/lib/patches/capacitorFetch.ts | 217 ++++++++++++++++++ .../src/lib/patches/poTokenAndroid.ts | 2 +- materialious/src/lib/patches/youtubejs.ts | 8 + 3 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 materialious/src/lib/patches/capacitorFetch.ts diff --git a/materialious/src/lib/patches/capacitorFetch.ts b/materialious/src/lib/patches/capacitorFetch.ts new file mode 100644 index 00000000..26dea032 --- /dev/null +++ b/materialious/src/lib/patches/capacitorFetch.ts @@ -0,0 +1,217 @@ +import type { HttpResponse } from '@capacitor/core'; +import { CapacitorHttp } from '@capacitor/core'; +import type { CapFormDataEntry, WindowCapacitor } from '@capacitor/core/types/definitions-internal'; + +// Rip from https://github.com/ionic-team/capacitor/blob/912d532cf6c7424d180b185120e7a9ba9c1ae050/core/native-bridge.ts +// with some modifications. + +const CAPACITOR_HTTP_INTERCEPTOR = '/_capacitor_http_interceptor_'; +const CAPACITOR_HTTP_INTERCEPTOR_URL_PARAM = 'u'; + +const isRelativeOrProxyUrl = (url: string | undefined): boolean => + !url || + !(url.startsWith('http:') || url.startsWith('https:')) || + url.indexOf(CAPACITOR_HTTP_INTERCEPTOR) > -1; + +const createProxyUrl = (url: string, win: WindowCapacitor): string => { + if (isRelativeOrProxyUrl(url)) return url; + const bridgeUrl = new URL(win.Capacitor?.getServerUrl() ?? ''); + bridgeUrl.pathname = CAPACITOR_HTTP_INTERCEPTOR; + bridgeUrl.searchParams.append(CAPACITOR_HTTP_INTERCEPTOR_URL_PARAM, url); + + return bridgeUrl.toString(); +}; + +const readFileAsBase64 = (file: File): Promise => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => { + const data = reader.result as string; + resolve(btoa(data)); + }; + reader.onerror = reject; + + reader.readAsBinaryString(file); + }); + +const convertFormData = async (formData: FormData): Promise => { + const newFormData: CapFormDataEntry[] = []; + for (const pair of formData.entries()) { + const [key, value] = pair; + if (value instanceof File) { + const base64File = await readFileAsBase64(value); + newFormData.push({ + key, + value: base64File, + type: 'base64File', + contentType: value.type, + fileName: value.name, + }); + } else { + newFormData.push({ key, value, type: 'string' }); + } + } + + return newFormData; +}; + +const convertBody = async ( + body: Document | XMLHttpRequestBodyInit | ReadableStream | undefined, + contentType?: string, +): Promise => { + if (body instanceof ReadableStream || body instanceof Uint8Array) { + let encodedData; + if (body instanceof ReadableStream) { + const reader = body.getReader(); + const chunks: any[] = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); + } + const concatenated = new Uint8Array( + chunks.reduce((acc, chunk) => acc + chunk.length, 0), + ); + let position = 0; + for (const chunk of chunks) { + concatenated.set(chunk, position); + position += chunk.length; + } + encodedData = concatenated; + } else { + encodedData = body; + } + + let data = new TextDecoder().decode(encodedData); + let type; + if (contentType === 'application/json') { + try { + data = JSON.parse(data); + } catch (ignored) { + // ignore + } + type = 'json'; + } else if (contentType === 'multipart/form-data') { + type = 'formData'; + } else if (contentType?.startsWith('image')) { + type = 'image'; + } else if (contentType === 'application/octet-stream') { + type = 'binary'; + } else { + type = 'text'; + } + + return { + data, + type, + headers: { 'Content-Type': contentType || 'application/octet-stream' }, + }; + } else if (body instanceof URLSearchParams) { + return { + data: body.toString(), + type: 'text', + }; + } else if (body instanceof FormData) { + const formData = await convertFormData(body); + return { + data: formData, + type: 'formData', + }; + } else if (body instanceof File) { + const fileData = await readFileAsBase64(body); + return { + data: fileData, + type: 'file', + headers: { 'Content-Type': body.type }, + }; + } + + return { data: body, type: 'json' }; +}; + +export const capacitorFetch = async ( + resource: RequestInfo | URL, + options?: RequestInit, +) => { + const request = new Request(resource, options); + const { method } = request; + if ( + method.toLocaleUpperCase() === 'GET' || + method.toLocaleUpperCase() === 'HEAD' || + method.toLocaleUpperCase() === 'OPTIONS' || + method.toLocaleUpperCase() === 'TRACE' + ) { + if (typeof resource === 'string') { + return await fetch( + createProxyUrl(resource, window as WindowCapacitor), + options, + ); + } else if (resource instanceof Request) { + const modifiedRequest = new Request( + createProxyUrl(resource.url, window as WindowCapacitor), + resource, + ); + return await fetch(modifiedRequest, options); + } + } + + const tag = `CapacitorHttp fetch ${Date.now()} ${resource}`; + console.time(tag); + + try { + const { body } = request; + const optionHeaders = Object.fromEntries(request.headers.entries()); + const { + data: requestData, + type, + headers, + } = await convertBody( + options?.body || body || undefined, + optionHeaders['Content-Type'] || optionHeaders['content-type'], + ); + + const nativeResponse: HttpResponse = await CapacitorHttp.request({ + url: request.url, + method: method, + data: requestData, + dataType: type, + headers: { + ...headers, + ...optionHeaders, + }, + }); + + const contentType = + nativeResponse.headers['Content-Type'] || + nativeResponse.headers['content-type']; + let data = contentType?.startsWith('application/json') + ? JSON.stringify(nativeResponse.data) + : nativeResponse.data; + + // use null data for 204 No Content HTTP response + if (nativeResponse.status === 204) { + data = null; + } + + // intercept & parse response before returning + const response = new Response(data, { + headers: nativeResponse.headers, + status: nativeResponse.status, + }); + + /* + * copy url to response, `cordova-plugin-ionic` uses this url from the response + * we need `Object.defineProperty` because url is an inherited getter on the Response + * see: https://stackoverflow.com/a/57382543 + * */ + Object.defineProperty(response, 'url', { + value: nativeResponse.url, + }); + + console.timeEnd(tag); + return response; + } catch (error) { + console.timeEnd(tag); + return Promise.reject(error); + } +}; \ No newline at end of file diff --git a/materialious/src/lib/patches/poTokenAndroid.ts b/materialious/src/lib/patches/poTokenAndroid.ts index e05bc93a..0d0533c9 100644 --- a/materialious/src/lib/patches/poTokenAndroid.ts +++ b/materialious/src/lib/patches/poTokenAndroid.ts @@ -115,7 +115,7 @@ attemptClickPlayButton(); InAppBrowser.openWebView({ url: 'https://www.youtube.com/embed/jNQXAC9IVRw', - title: 'Pulling po tokens', + title: 'Pulling po tokens (This may take a moment)', headers: headers }); }); diff --git a/materialious/src/lib/patches/youtubejs.ts b/materialious/src/lib/patches/youtubejs.ts index ed0e6d6f..9c5fae02 100644 --- a/materialious/src/lib/patches/youtubejs.ts +++ b/materialious/src/lib/patches/youtubejs.ts @@ -3,6 +3,7 @@ import { numberWithCommas } from '$lib/misc'; import { poTokenCacheStore } from '$lib/store'; import { Capacitor } from '@capacitor/core'; import { get } from 'svelte/store'; +import { capacitorFetch } from './capacitorFetch'; import { type PoTokens } from './poTokenAndroid'; export async function patchYoutubeJs(videoId: string): Promise { @@ -28,6 +29,13 @@ export async function patchYoutubeJs(videoId: string): Promise { const youtube = await innertube.create({ visitor_data: tokens.visitor_data, po_token: tokens.po_token, + fetch: async (input: RequestInfo | URL, init?: RequestInit) => { + if (Capacitor.getPlatform() === 'android') { + return capacitorFetch(input, init); + } else { + return fetch(input, init); + } + } }); const video = await youtube.getInfo(videoId); From 64ad345650144f614a995b14c598680cdbc2a851 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Sun, 22 Sep 2024 02:07:42 +1200 Subject: [PATCH 4/4] Bump version --- materialious/android/app/build.gradle | 4 ++-- materialious/electron/package.json | 2 +- materialious/package.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/materialious/android/app/build.gradle b/materialious/android/app/build.gradle index 195611f4..7cbace8a 100644 --- a/materialious/android/app/build.gradle +++ b/materialious/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "us.materialio.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 45 - versionName "1.5.4" + versionCode 46 + versionName "1.5.5" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/materialious/electron/package.json b/materialious/electron/package.json index b419410c..f5ffe4e9 100644 --- a/materialious/electron/package.json +++ b/materialious/electron/package.json @@ -1,6 +1,6 @@ { "name": "Materialious", - "version": "1.5.4", + "version": "1.5.5", "description": "Modern material design for Invidious.", "author": { "name": "Ward Pearce", diff --git a/materialious/package.json b/materialious/package.json index 01d7502d..5e7a768b 100644 --- a/materialious/package.json +++ b/materialious/package.json @@ -1,6 +1,6 @@ { "name": "materialious", - "version": "1.5.4", + "version": "1.5.5", "private": true, "scripts": { "dev": "vite dev", @@ -72,4 +72,4 @@ "terser": "^5.33.0", "vidstack": "^1.12.9" } -} +} \ No newline at end of file