From af8fc1db2ca37d8a86b8297f4ba56428acab5ca8 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Tue, 17 Sep 2024 17:08:08 +1200 Subject: [PATCH 1/8] Update 1.4.0 --- materialious/android/app/build.gradle | 4 ++-- materialious/electron/package.json | 4 ++-- materialious/package.json | 2 +- update_versions.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/materialious/android/app/build.gradle b/materialious/android/app/build.gradle index a1c22fbe..c24798ed 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 36 - versionName "1.3.7" + versionCode 37 + versionName "1.4.0" 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 178bf44a..37da5c3b 100644 --- a/materialious/electron/package.json +++ b/materialious/electron/package.json @@ -1,6 +1,6 @@ { "name": "Materialious", - "version": "1.3.7", + "version": "1.4.0", "description": "Modern material design for Invidious.", "author": { "name": "Ward Pearce", @@ -39,4 +39,4 @@ "capacitor", "electron" ] -} +} \ No newline at end of file diff --git a/materialious/package.json b/materialious/package.json index 6b2243af..9bc56625 100644 --- a/materialious/package.json +++ b/materialious/package.json @@ -1,6 +1,6 @@ { "name": "materialious", - "version": "1.3.7", + "version": "1.4.0", "private": true, "scripts": { "dev": "vite dev", diff --git a/update_versions.py b/update_versions.py index 4d80dc12..ad4ad5f1 100644 --- a/update_versions.py +++ b/update_versions.py @@ -5,7 +5,7 @@ import json import os import re -LATEST_VERSION = "1.3.7" +LATEST_VERSION = "1.4.0" WORKING_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "materialious") ROOT_PACKAGE = os.path.join(WORKING_DIR, "package.json") From 3864b2aab054421ee896bf3a2937cd01125a1090 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Tue, 17 Sep 2024 17:35:25 +1200 Subject: [PATCH 2/8] Fix image loading --- docs/DOCKER.md | 40 +++++++++++++++++-- materialious/src/lib/ChannelThumbnail.svelte | 12 ++++-- materialious/src/lib/Comment.svelte | 20 ++++++---- materialious/src/lib/Thumbnail.svelte | 16 ++++---- materialious/src/lib/misc.ts | 5 --- .../routes/(app)/watch/[slug]/+page.svelte | 9 +++-- 6 files changed, 72 insertions(+), 30 deletions(-) diff --git a/docs/DOCKER.md b/docs/DOCKER.md index c3924769..3ab4cf21 100644 --- a/docs/DOCKER.md +++ b/docs/DOCKER.md @@ -276,24 +276,32 @@ VITE_DEFAULT_PEERJS_PORT: 443 ## Step 7 (Optional): Enabling downloads -### Step 1: Add the following to your reverse proxy +### Step 1: Add the following to your reverse proxies #### Caddy ```caddy +# Materialious materialious.example.com { reverse_proxy localhost:3001 header { - Cross-Origin-Opener-Policy "same-origin" - Cross-Origin-Embedder-Policy "require-corp" + Cross-Origin-Opener-Policy "same-origin" + Cross-Origin-Embedder-Policy "require-corp" } +} +# Invidious +invidious.example.com { + # Other configurations + + header /ggpht/* Cross-Origin-Resource-Policy cross-origin } ``` #### Nginx ```nginx +# Materialious server { listen 80; server_name materialious.example.com; @@ -305,12 +313,23 @@ server { proxy_pass http://localhost:3001; } } + +# Invidious +server { + listen 80; + server_name invidious.example.com; + + location /ggpht/ { + add_header Cross-Origin-Resource-Policy cross-origin; + } +} ``` #### Traefik Add the following to Materialious ```yaml +# Materialious http: middlewares: materialious-downloads: @@ -318,6 +337,21 @@ http: customResponseHeaders: Cross-Origin-Opener-Policy: "same-origin" Cross-Origin-Embedder-Policy: "require-corp" + +# Invidious +http: + routers: + ggpht-router: + rule: "PathPrefix(`/ggpht/`)" + service: ggpht-service + middlewares: + - headers-ggpht + + middlewares: + headers-ggpht: + headers: + customResponseHeaders: + Cross-Origin-Resource-Policy: cross-origin ``` diff --git a/materialious/src/lib/ChannelThumbnail.svelte b/materialious/src/lib/ChannelThumbnail.svelte index c91de580..10cc7a7b 100644 --- a/materialious/src/lib/ChannelThumbnail.svelte +++ b/materialious/src/lib/ChannelThumbnail.svelte @@ -6,11 +6,15 @@ export let channel: Channel; - let channelPfp: string; + let channelPfp: HTMLImageElement | undefined; onMount(async () => { - const pfpResp = await fetch(proxyGoogleImage(getBestThumbnail(channel.authorThumbnails))); - channelPfp = URL.createObjectURL(await pfpResp.blob()); + const img = new Image(); + img.src = proxyGoogleImage(getBestThumbnail(channel.authorThumbnails)); + + img.onload = () => { + channelPfp = img; + }; }); @@ -23,7 +27,7 @@ {channel.author} {/if} diff --git a/materialious/src/lib/Comment.svelte b/materialious/src/lib/Comment.svelte index 7412c989..a99f3ec1 100644 --- a/materialious/src/lib/Comment.svelte +++ b/materialious/src/lib/Comment.svelte @@ -2,7 +2,7 @@ import { _ } from 'svelte-i18n'; import { getComments } from './Api'; import { type Comment, type Comments } from './Api/model'; - import { getBestThumbnail, loadPfp, numberWithCommas, proxyGoogleImage } from './misc'; + import { getBestThumbnail, numberWithCommas, proxyGoogleImage } from './misc'; export let comment: Comment; export let videoId: string; @@ -31,9 +31,12 @@
- {#await loadPfp(proxyGoogleImage(getBestThumbnail(comment.authorThumbnails))) then pfp} - comment profile - {/await} + comment profile +
@@ -58,9 +61,12 @@

thumb_up {numberWithCommas(comment.likeCount)}

{#if comment.creatorHeart}
- {#await loadPfp(proxyGoogleImage(comment.creatorHeart.creatorThumbnail)) then pfp} - Creator profile - {/await} + Creator profile favorite diff --git a/materialious/src/lib/Thumbnail.svelte b/materialious/src/lib/Thumbnail.svelte index 7376a260..9ca874d0 100644 --- a/materialious/src/lib/Thumbnail.svelte +++ b/materialious/src/lib/Thumbnail.svelte @@ -39,7 +39,7 @@ let videoPreviewVolume: number = 0.4; let imgHeight: number; - let authorImg: string = ''; + let authorImg: HTMLImageElement | undefined; let proxyVideos = get(playerProxyVideosStore); @@ -85,10 +85,12 @@ async function loadAuthor() { const channel = await getChannel(video.authorId); - const imgResp = await fetch( - proxyGoogleImage(getBestThumbnail(channel.authorThumbnails, 75, 75)) - ); - authorImg = URL.createObjectURL(await imgResp.blob()); + const img = new Image(); + img.src = proxyGoogleImage(getBestThumbnail(channel.authorThumbnails, 75, 75)); + + img.onload = () => { + authorImg = img; + }; } onMount(async () => { @@ -353,8 +355,8 @@
{#if !sideways}
- {#if authorImg !== ''} - Author + {#if authorImg} + Author {:else} {/if} diff --git a/materialious/src/lib/misc.ts b/materialious/src/lib/misc.ts index 7e6c2caa..b27cea22 100644 --- a/materialious/src/lib/misc.ts +++ b/materialious/src/lib/misc.ts @@ -170,11 +170,6 @@ export function proxyVideoUrl(source: string): string { return rawSrc.toString(); } -export async function loadPfp(url: string): Promise { - const resp = await fetch(url); - return URL.createObjectURL(await resp.blob()); -} - export function pullBitratePreference(): number { const vidstack = localStorage.getItem('video-player'); diff --git a/materialious/src/routes/(app)/watch/[slug]/+page.svelte b/materialious/src/routes/(app)/watch/[slug]/+page.svelte index c3f2b42c..9b25c56d 100644 --- a/materialious/src/routes/(app)/watch/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/watch/[slug]/+page.svelte @@ -45,7 +45,6 @@ import { _ } from 'svelte-i18n'; import { get } from 'svelte/store'; import type { MediaPlayerElement } from 'vidstack/elements'; - import { loadPfp } from '../../../../lib/misc.js'; export let data; @@ -487,9 +486,11 @@
diff --git a/materialious/src/lib/i18n/locales/en.json b/materialious/src/lib/i18n/locales/en.json index d7bff6a3..5d687f8f 100644 --- a/materialious/src/lib/i18n/locales/en.json +++ b/materialious/src/lib/i18n/locales/en.json @@ -123,7 +123,8 @@ "proxyVideos": "Proxy videos", "backgroundPlay": "Play audio in background", "dash": "Dash", - "silenceSkipper": "Silence skipper (Experimental)" + "silenceSkipper": "Silence skipper (Experimental)", + "youtubeJsFallback": "Locally video processing fallback" }, "bookmarklet": "Bookmarklet", "instanceUrl": "Instance URL", diff --git a/materialious/src/lib/patches/youtubejs.ts b/materialious/src/lib/patches/youtubejs.ts new file mode 100644 index 00000000..fa143aec --- /dev/null +++ b/materialious/src/lib/patches/youtubejs.ts @@ -0,0 +1,93 @@ +import type { Image, Thumbnail, VideoBase, VideoPlay } from '$lib/Api/model'; +import { numberWithCommas } from '$lib/misc'; +import { playerYouTubeJsPoToken, playerYouTubeJsVisitorData } from '$lib/store'; +import { get } from 'svelte/store'; + +export async function patchYoutubeJs(videoId: string): Promise { + const innertube = (await import('youtubei.js')).Innertube; + + let innertubeConfig: Record = { + visitor_data: get(playerYouTubeJsVisitorData), + po_token: get(playerYouTubeJsPoToken) + }; + + if (innertubeConfig.visitor_data === '' || innertubeConfig.po_token === '') { + innertubeConfig = {}; + } + + const youtube = await innertube.create(innertubeConfig); + + const video = await youtube.getInfo(videoId); + + if (!video.primary_info || !video.secondary_info) { + throw new Error('Unable to pull video info from youtube.js'); + } + + let manifest = await video.toDash(); + + // Hack to fix video not displaying. + // Thanks to absidue & Andrews54757 + manifest = manifest.replaceAll(') => { + recommendedVideos.push({ + videoThumbnails: recommended?.thumbnails as Thumbnail[] || [], + videoId: recommended?.id || '', + title: recommended?.title.toString() || '', + viewCountText: numberWithCommas(Number(recommended?.view_count.toString().replace(/\D/g, '') || 0)) || '', + lengthSeconds: recommended?.duration?.seconds || 0, + author: recommended?.author.name || '', + authorId: recommended?.author.id || '' + }); + }); + + 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(), + likeCount: video.basic_info.like_count || 0, + dislikeCount: 0, + allowRatings: false, + rating: 0, + isListed: 0, + isFamilyFriendly: video.basic_info.is_family_safe || true, + genre: video.basic_info.category || '', + genreUrl: '', + dashUrl: dashUri, + adaptiveFormats: [], + formatStreams: [], + recommendedVideos: recommendedVideos, + authorThumbnails: authorThumbnails, + captions: [], + authorId: video.basic_info.channel_id || '', + authorUrl: `/channel/${video.basic_info.channel_id}`, + authorVerified: false, + description: descString, + descriptionHtml: video.secondary_info.description.toHTML() || descString, + published: 0, + publishedText: video.primary_info.published.toString(), + premiereTimestamp: 0, + liveNow: false, + premium: false, + isUpcoming: false, + videoId: videoId, + videoThumbnails: video.basic_info.thumbnail as Thumbnail[], + author: video.basic_info.author || 'Unknown', + lengthSeconds: video.basic_info.duration || 0, + subCountText: '', + keywords: video.basic_info.keywords || [], + allowedRegions: [] + }; +} \ No newline at end of file diff --git a/materialious/src/lib/store.ts b/materialious/src/lib/store.ts index a388fafa..a48c000e 100644 --- a/materialious/src/lib/store.ts +++ b/materialious/src/lib/store.ts @@ -39,6 +39,9 @@ export const playerTheatreModeByDefaultStore = persisted('theatreModeByDefault', 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 playerYouTubeJsVisitorData = persisted('youTubeJsVisitorData', ''); +export const playerYouTubeJsPoToken = persisted('youTubeJsPoToken', ''); export const returnYtDislikesStore = persisted('returnYtDislikes', false); export const returnYTDislikesInstanceStore: Writable = persisted( From 277e160c2de12de202b15bbafcbcec15483c65ef Mon Sep 17 00:00:00 2001 From: WardPearce Date: Tue, 17 Sep 2024 21:56:03 +1200 Subject: [PATCH 6/8] Generate po tokens for electron --- README.md | 2 +- materialious/electron/package.json | 5 +-- materialious/electron/src/preload.ts | 8 +++-- materialious/electron/src/setup.ts | 12 ++++++- materialious/src/lib/Api/index.ts | 2 +- materialious/src/lib/Settings.svelte | 38 +---------------------- materialious/src/lib/patches/youtubejs.ts | 16 +++------- materialious/src/lib/store.ts | 2 -- 8 files changed, 27 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 611b17a3..1451e9fe 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ - Mini player. - Silence skipper (Experimental.) - ffmpeg integration for downloading videos with audio at any quality ([Configuration required](./docs/DOCKER.md#step-7-optional-enabling-downloads)). -- [YouTube.js](https://github.com/LuanRT/YouTube.js) fallback if Invidious fails loading videos for Desktop & Android. +- [YouTube.js](https://github.com/LuanRT/YouTube.js) fallback if Invidious fails loading videos for Desktop. - Preview video on hover. - Sponsorblock built-in. - Return YouTube dislikes built-in. diff --git a/materialious/electron/package.json b/materialious/electron/package.json index 37da5c3b..cc127024 100644 --- a/materialious/electron/package.json +++ b/materialious/electron/package.json @@ -27,7 +27,8 @@ "electron-unhandled": "^4.0.1", "electron-updater": "^6.3.4", "electron-window-state": "^5.0.3", - "jsonfile": "^6.1.0" + "jsonfile": "^6.1.0", + "youtube-po-token-generator": "^0.2.0" }, "devDependencies": { "electron": "^32.1.0", @@ -39,4 +40,4 @@ "capacitor", "electron" ] -} \ No newline at end of file +} diff --git a/materialious/electron/src/preload.ts b/materialious/electron/src/preload.ts index c817d3b7..92641d9c 100644 --- a/materialious/electron/src/preload.ts +++ b/materialious/electron/src/preload.ts @@ -1,4 +1,6 @@ require('./rt/electron-rt'); -////////////////////////////// -// User Defined Preload scripts below -console.log('User Preload!'); +const { contextBridge, ipcRenderer } = require('electron'); + +contextBridge.exposeInMainWorld('electron', { + generatePoToken: () => ipcRenderer.invoke('generate-po-token') +}); diff --git a/materialious/electron/src/setup.ts b/materialious/electron/src/setup.ts index dbfe60b2..2e265b8d 100644 --- a/materialious/electron/src/setup.ts +++ b/materialious/electron/src/setup.ts @@ -6,11 +6,12 @@ import { } from '@capacitor-community/electron'; import chokidar from 'chokidar'; import type { MenuItemConstructorOptions } from 'electron'; -import { app, BrowserWindow, Menu, MenuItem, nativeImage, session, Tray } from 'electron'; +import { app, BrowserWindow, ipcMain, Menu, MenuItem, nativeImage, session, Tray } from 'electron'; import electronIsDev from 'electron-is-dev'; import electronServe from 'electron-serve'; import windowStateKeeper from 'electron-window-state'; import { join } from 'path'; +import { generate } from 'youtube-po-token-generator'; // Define components for a watcher to detect when the webapp is changed so we can reload in Dev mode. const reloadWatcher = { @@ -214,6 +215,15 @@ export class ElectronCapacitorApp { CapElectronEventEmitter.emit('CAPELECTRON_DeeplinkListenerInitialized', ''); }, 400); }); + + ipcMain.handle('generate-po-token', async () => { + try { + return await generate(); + } catch (error) { + console.error('Error generating token:', error); + return { error: error.message }; // Return error to renderer + } + }); } } diff --git a/materialious/src/lib/Api/index.ts b/materialious/src/lib/Api/index.ts index 1956260d..d4c05e0f 100644 --- a/materialious/src/lib/Api/index.ts +++ b/materialious/src/lib/Api/index.ts @@ -72,7 +72,7 @@ export async function getPopular(): Promise { export async function getVideo(videoId: string, local: boolean = false): Promise { const resp = await fetch(setRegion(buildPath(`videos/${videoId}?local=${local}`))); - if (!resp.ok && Capacitor.getPlatform() !== 'web' && get(playerYouTubeJsFallback)) { + if (!resp.ok && Capacitor.getPlatform() === 'electron' && get(playerYouTubeJsFallback)) { return await patchYoutubeJs(videoId); } else { await fetchErrorHandle(resp); diff --git a/materialious/src/lib/Settings.svelte b/materialious/src/lib/Settings.svelte index 3684356b..d91889a0 100644 --- a/materialious/src/lib/Settings.svelte +++ b/materialious/src/lib/Settings.svelte @@ -31,8 +31,6 @@ playerSavePlaybackPositionStore, playerTheatreModeByDefaultStore, playerYouTubeJsFallback, - playerYouTubeJsPoToken, - playerYouTubeJsVisitorData, returnYTDislikesInstanceStore, returnYtDislikesStore, silenceSkipperStore, @@ -50,8 +48,6 @@ sponsorBlockCategoriesStore.subscribe((value) => (sponsorCategoriesList = value)); let sponsorBlockInstance = get(sponsorBlockUrlStore); - let youTubeJsVisitorData = get(playerYouTubeJsVisitorData); - let youTubeJsPoToken = get(playerYouTubeJsPoToken); let synciousInstance = get(synciousInstanceStore); let returnYTInstance = get(returnYTDislikesInstanceStore); let invidiousInstance = get(instanceStore); @@ -445,7 +441,7 @@
- {#if Capacitor.getPlatform() !== 'web'} + {#if Capacitor.getPlatform() === 'electron'}
- -
{ - playerYouTubeJsVisitorData.set(youTubeJsVisitorData); - }} - > - -
- -
{ - playerYouTubeJsPoToken.set(youTubeJsPoToken); - }} - > - -
{/if}
diff --git a/materialious/src/lib/patches/youtubejs.ts b/materialious/src/lib/patches/youtubejs.ts index fa143aec..dd96ec86 100644 --- a/materialious/src/lib/patches/youtubejs.ts +++ b/materialious/src/lib/patches/youtubejs.ts @@ -1,21 +1,15 @@ import type { Image, Thumbnail, VideoBase, VideoPlay } from '$lib/Api/model'; import { numberWithCommas } from '$lib/misc'; -import { playerYouTubeJsPoToken, playerYouTubeJsVisitorData } from '$lib/store'; -import { get } from 'svelte/store'; export async function patchYoutubeJs(videoId: string): Promise { const innertube = (await import('youtubei.js')).Innertube; - let innertubeConfig: Record = { - visitor_data: get(playerYouTubeJsVisitorData), - po_token: get(playerYouTubeJsPoToken) - }; + const tokens = await window.electron.generatePoToken(); - if (innertubeConfig.visitor_data === '' || innertubeConfig.po_token === '') { - innertubeConfig = {}; - } - - const youtube = await innertube.create(innertubeConfig); + const youtube = await innertube.create({ + visitor_data: tokens.visitorData, + po_token: tokens.poToken + }); const video = await youtube.getInfo(videoId); diff --git a/materialious/src/lib/store.ts b/materialious/src/lib/store.ts index a48c000e..37055dc5 100644 --- a/materialious/src/lib/store.ts +++ b/materialious/src/lib/store.ts @@ -40,8 +40,6 @@ export const playerAutoplayNextByDefaultStore = persisted('autoplayNextByDefault export const playerMiniPlayerStore = persisted('miniPlayer', true); export const playerAndroidBackgroundPlayStore = persisted('androidBackgroundPlayer', true); export const playerYouTubeJsFallback = persisted('youTubeJsFallback', true); -export const playerYouTubeJsVisitorData = persisted('youTubeJsVisitorData', ''); -export const playerYouTubeJsPoToken = persisted('youTubeJsPoToken', ''); export const returnYtDislikesStore = persisted('returnYtDislikes', false); export const returnYTDislikesInstanceStore: Writable = persisted( From 408ec8f484cb39ec5ab672fce5ee8a16ae8ad97b Mon Sep 17 00:00:00 2001 From: WardPearce Date: Tue, 17 Sep 2024 21:57:00 +1200 Subject: [PATCH 7/8] fix lint error --- materialious/src/lib/patches/youtubejs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/materialious/src/lib/patches/youtubejs.ts b/materialious/src/lib/patches/youtubejs.ts index dd96ec86..b081eb0f 100644 --- a/materialious/src/lib/patches/youtubejs.ts +++ b/materialious/src/lib/patches/youtubejs.ts @@ -4,7 +4,7 @@ import { numberWithCommas } from '$lib/misc'; export async function patchYoutubeJs(videoId: string): Promise { const innertube = (await import('youtubei.js')).Innertube; - const tokens = await window.electron.generatePoToken(); + const tokens = await (window as any).electron.generatePoToken(); const youtube = await innertube.create({ visitor_data: tokens.visitorData, From 95c14ec5c61b638420b74f9a3f6103ee72be8382 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Tue, 17 Sep 2024 21:59:05 +1200 Subject: [PATCH 8/8] Dont preview video if no format stream --- materialious/src/lib/Thumbnail.svelte | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/materialious/src/lib/Thumbnail.svelte b/materialious/src/lib/Thumbnail.svelte index 9ca874d0..6d3c38ee 100644 --- a/materialious/src/lib/Thumbnail.svelte +++ b/materialious/src/lib/Thumbnail.svelte @@ -230,6 +230,10 @@ showVideoPreview = true; try { videoPreview = await getVideo(video.videoId); + if (videoPreview.formatStreams.length === 0) { + showVideoPreview = false; + } + if (videoPreview.hlsUrl) { showVideoPreview = false; videoPreview = null;