+
search
+
{
+ search = history;
+ }}
+ href={`/search/${encodeURIComponent(history)}`}
+ >
{history}
{/each}
diff --git a/materialious/src/lib/components/Settings/Player.svelte b/materialious/src/lib/components/Settings/Player.svelte
index a1988969..735aa191 100644
--- a/materialious/src/lib/components/Settings/Player.svelte
+++ b/materialious/src/lib/components/Settings/Player.svelte
@@ -6,7 +6,6 @@
import { get } from 'svelte/store';
import {
playerAlwaysLoopStore,
- playerAndroidBgPlayer,
playerAndroidLockOrientation,
playerAutoPlayStore,
playerAutoplayNextByDefaultStore,
@@ -16,8 +15,7 @@
playerSavePlaybackPositionStore,
playerTheatreModeByDefaultStore,
playerYouTubeJsAlways,
- playerYouTubeJsFallback,
- silenceSkipperStore
+ playerYouTubeJsFallback
} from '../../store';
let defaultLanguage = $state(get(playerDefaultLanguage));
@@ -77,22 +75,6 @@
{#if Capacitor.getPlatform() === 'android'}
-
-
-
-
diff --git a/materialious/src/lib/components/Transcript.svelte b/materialious/src/lib/components/Transcript.svelte
index ff865fc3..79a11973 100644
--- a/materialious/src/lib/components/Transcript.svelte
+++ b/materialious/src/lib/components/Transcript.svelte
@@ -4,18 +4,16 @@
import { VTTCue, parseText, type ParsedCaptionsResult } from 'media-captions';
import { _ } from 'svelte-i18n';
import { get } from 'svelte/store';
- import type { MediaTimeUpdateEvent } from 'vidstack';
- import type { MediaPlayerElement } from 'vidstack/elements';
import type { VideoPlay } from '../api/model';
import { decodeHtmlCharCodes } from '../misc';
import { instanceStore } from '../store';
interface Props {
video: VideoPlay;
- player: MediaPlayerElement;
+ playerElement: HTMLMediaElement;
}
- let { video, player = $bindable() }: Props = $props();
+ let { video, playerElement = $bindable() }: Props = $props();
let url: string | null = $state(null);
let autoScroll: boolean = $state(true);
@@ -26,8 +24,8 @@
let currentTime = $state(0);
let search: string = $state('');
- player.addEventListener('time-update', (event: MediaTimeUpdateEvent) => {
- currentTime = event.detail.currentTime;
+ playerElement.addEventListener('timeupdate', () => {
+ currentTime = playerElement.currentTime;
if (autoScroll) {
const currentTranscriptLine = document.querySelector(
@@ -115,7 +113,7 @@
(player.currentTime = cue.startTime)}
+ onclick={() => (playerElement.currentTime = cue.startTime)}
class:secondary-container={currentTime >= cue.startTime && currentTime <= cue.endTime}
>
{videoLength(cue.startTime)}
diff --git a/materialious/src/lib/style.css b/materialious/src/lib/css/global.css
similarity index 60%
rename from materialious/src/lib/style.css
rename to materialious/src/lib/css/global.css
index 4c1efbce..3cb92bb7 100644
--- a/materialious/src/lib/style.css
+++ b/materialious/src/lib/css/global.css
@@ -2,48 +2,6 @@
padding: 0.1em 0.4em;
}
-.vds-chapter-radio-label {
- word-wrap: break-word !important;
- overflow-wrap: break-word !important;
- white-space: normal !important;
-}
-
-.vds-chapter-radio-content {
- max-width: 200px !important;
-}
-
-.vds-kb-action {
- position: absolute !important;
- top: 50% !important;
- left: 50% !important;
-}
-
-.vds-chapter-radio {
- align-items: start !important;
-}
-
-.vds-time-divider {
- font-size: 0 !important;
- margin-right: 5px !important;
-}
-
-.sponsorskip-bar {
- position: absolute;
- margin-top: 2px;
- height: 5px;
- top: -1;
- border-radius: 1em;
-}
-
-.vds-time-slider {
- position: relative !important;
- width: 100% !important;
-}
-
-.vds-poster :where(img) {
- object-fit: cover !important;
-}
-
.infinite-status-prompt {
display: none !important;
}
@@ -59,6 +17,25 @@ main.root {
overflow-y: auto;
}
+article, dialog {
+ border-radius: .25rem !important;
+}
+
+.row {
+ padding: 1em !important;
+}
+
+button.row {
+ padding: 0 !important;
+ background-color: transparent !important;
+ border: none !important;
+ color: var(--on-surface) !important;
+}
+
+button.row:hover {
+ box-shadow: none !important;
+ background-color: transparent !important;
+}
@media screen and (max-width: 650px) {
dialog.right {
diff --git a/materialious/src/lib/externalSettings.ts b/materialious/src/lib/externalSettings.ts
index 6df07094..362ac66c 100644
--- a/materialious/src/lib/externalSettings.ts
+++ b/materialious/src/lib/externalSettings.ts
@@ -25,7 +25,6 @@ import {
returnYTDislikesInstanceStore,
returnYtDislikesStore,
showWarningStore,
- silenceSkipperStore,
sponsorBlockCategoriesStore,
sponsorBlockDisplayToastStore,
sponsorBlockStore,
@@ -167,11 +166,6 @@ const persistedStores: { name: string, store: Writable
, type: 'string' | 'b
store: sponsorBlockDisplayToastStore,
type: 'boolean'
},
- {
- name: 'silenceSkipper',
- store: silenceSkipperStore,
- type: 'boolean'
- },
{
name: 'amoledTheme',
store: interfaceAmoledTheme,
diff --git a/materialious/src/lib/misc.ts b/materialious/src/lib/misc.ts
index 14055d8b..a4f00eba 100644
--- a/materialious/src/lib/misc.ts
+++ b/materialious/src/lib/misc.ts
@@ -29,20 +29,6 @@ export function proxyVideoUrl(source: string): string {
return rawSrc.toString();
}
-export function pullBitratePreference(): number {
- const vidstack = localStorage.getItem('video-player');
-
- if (vidstack) {
- const vidstackSettings = JSON.parse(vidstack);
- if (vidstackSettings.quality && vidstackSettings.quality.bitrate) {
- return vidstackSettings.quality.bitrate;
- }
- }
-
- return -1;
-}
-
-
export function unsafeRandomItem(array: any[]): any {
return array[Math.floor(Math.random() * array.length)];
}
diff --git a/materialious/src/lib/patches/youtubejs.ts b/materialious/src/lib/patches/youtubejs.ts
index 88e2d693..0e1864b0 100644
--- a/materialious/src/lib/patches/youtubejs.ts
+++ b/materialious/src/lib/patches/youtubejs.ts
@@ -1,136 +1,87 @@
+import { androidPoTokenMinter } from '$lib/android/youtube/minter';
import type { AdaptiveFormats, Captions, Image, StoryBoard, Thumbnail, VideoBase, VideoPlay } from '$lib/api/model';
import { interfaceRegionStore, poTokenCacheStore } from '$lib/store';
import { numberWithCommas } from '$lib/time';
import { Capacitor } from '@capacitor/core';
-import type { WebPoSignalOutput } from 'bgutils-js';
-import { BG, buildURL, GOOG_API_KEY } from 'bgutils-js';
+import { USER_AGENT } from 'bgutils-js';
+import { Buffer } from 'buffer';
import { get } from 'svelte/store';
-import { Innertube, UniversalCache } from 'youtubei.js';
-import { capacitorFetch } from '../android/http/capacitorFetch';
-
-type WebPoMinter = {
- integrityTokenBasedMinter?: BG.WebPoMinter;
- botguardClient?: BG.BotGuardClient;
-};
-
-async function getWebPoMinter(): Promise {
- const requestKey = 'O43z0dpjhgX20SCx4KAo';
-
- const challengeResponse = await fetch(buildURL('Create', true), {
- method: 'POST',
- headers: {
- 'content-type': 'application/json+protobuf',
- 'x-goog-api-key': GOOG_API_KEY,
- 'x-user-agent': 'grpc-web-javascript/0.1'
- },
- body: JSON.stringify([requestKey])
- });
-
- const challengeResponseData = await challengeResponse.json();
-
- const bgChallenge = BG.Challenge.parseChallengeData(challengeResponseData);
-
- if (!bgChallenge)
- throw new Error('Could not get challenge');
-
- const interpreterJavascript = bgChallenge.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue;
-
- if (!document.getElementById(bgChallenge.interpreterHash)) {
- const script = document.createElement('script');
- script.type = 'text/javascript';
- script.id = bgChallenge.interpreterHash;
- script.textContent = interpreterJavascript;
- document.head.appendChild(script);
- }
-
- const botguardClient = await BG.BotGuardClient.create({
- globalObj: globalThis,
- globalName: bgChallenge.globalName,
- program: bgChallenge.program
- });
-
- if (bgChallenge) {
- const webPoSignalOutput: WebPoSignalOutput = [];
- const botguardResponse = await botguardClient.snapshot({ webPoSignalOutput });
-
- const integrityTokenResponse = await fetch(buildURL('GenerateIT', true), {
- method: 'POST',
- headers: {
- 'content-type': 'application/json+protobuf',
- 'x-goog-api-key': GOOG_API_KEY,
- 'x-user-agent': 'grpc-web-javacript/0.1'
- },
- body: JSON.stringify([requestKey, botguardResponse])
- });
-
- const integrityTokenResponseData = await integrityTokenResponse.json();
- const integrityToken = integrityTokenResponseData[0] as string | undefined;
-
- if (!integrityToken) {
- console.error('Could not get integrity token. Interpreter Hash:', bgChallenge.interpreterHash);
- return {};
- }
-
- const integrityTokenBasedMinter = await BG.WebPoMinter.create({ integrityToken }, webPoSignalOutput);
-
- return {
- integrityTokenBasedMinter,
- botguardClient
- };
- }
-
- return {};
-}
+import { Innertube, UniversalCache, YT, YTNodes } from 'youtubei.js';
export async function patchYoutubeJs(videoId: string): Promise {
if (!Capacitor.isNativePlatform()) {
- throw new Error('Platform not supported');
+ throw new Error('Yt.js: Platform not supported');
}
const youtube = await Innertube.create({
- fetch: Capacitor.getPlatform() === 'android' ? capacitorFetch : fetch,
- generate_session_locally: true,
+ fetch: fetch,
cache: new UniversalCache(false),
- location: get(interfaceRegionStore)
+ location: get(interfaceRegionStore),
+ user_agent: USER_AGENT,
+ enable_session_cache: false
});
- const { integrityTokenBasedMinter } = await getWebPoMinter();
+ let sessionPoToken: string;
+ let contentPoToken: string;
- let sessionWebPo: string | undefined;
- const poTokensCached = get(poTokenCacheStore);
- if (!poTokensCached && integrityTokenBasedMinter) {
- sessionWebPo = await integrityTokenBasedMinter.mintAsWebsafeString(youtube.session.context.client.visitorData ?? '');
- poTokenCacheStore.set(sessionWebPo);
+ const requestKey = 'O43z0dpjhgX20SCx4KAo';
+ const challengeResponse = await youtube.getAttestationChallenge('ENGAGEMENT_TYPE_UNBOUND');
+
+ if (Capacitor.getPlatform() === 'android') {
+ [sessionPoToken, contentPoToken] = await androidPoTokenMinter(
+ challengeResponse,
+ requestKey,
+ youtube.session.context.client.visitorData ?? '',
+ videoId
+ );
} else {
- sessionWebPo = poTokensCached;
+ [sessionPoToken, contentPoToken] = await window.electronAPI.generatePoToken(
+ challengeResponse,
+ requestKey,
+ youtube.session.context.client.visitorData ?? '',
+ videoId
+ );
}
- const video = await youtube.getInfo(videoId);
+ poTokenCacheStore.set(sessionPoToken);
+
+ const extraArgs: Record = {
+ playbackContext: {
+ contentPlaybackContext: {
+ vis: 0,
+ splay: false,
+ lactMilliseconds: '-1',
+ signatureTimestamp: youtube.session.player?.sts
+ }
+ },
+ serviceIntegrityDimensions: {
+ poToken: contentPoToken
+ },
+ contentCheckOk: true,
+ racyCheckOk: true
+ };
+
+ const watchEndpoint = new YTNodes.NavigationEndpoint({ watchEndpoint: { videoId } });
+ const rawPlayerResponse = await watchEndpoint.call(youtube.actions, extraArgs);
+ const rawNextResponse = await watchEndpoint.call(youtube.actions, {
+ override_endpoint: '/next',
+ racyCheckOk: true,
+ contentCheckOk: true
+ });
+
+ const video = new YT.VideoInfo([rawPlayerResponse, rawNextResponse], youtube!.actions, '');
+
+ console.log(video);
if (!video.primary_info || !video.secondary_info) {
- throw new Error('Unable to pull video info from youtube.js');
+ throw new Error('Yt.js: Unable to pull video info from youtube.js');
}
let dashUri: string = '';
if (!video.basic_info.is_live) {
- let manifest = await video.toDash();
-
- let parser = new DOMParser();
- let xmlDoc = parser.parseFromString(manifest, 'application/xml');
-
- let baseURLs = xmlDoc.getElementsByTagName('BaseURL');
-
- Array.from(baseURLs).forEach((baseURL) => {
- let url = new URL(baseURL.textContent as string);
- url.searchParams.set('pot', (sessionWebPo ?? BG.PoToken.generateColdStartToken(youtube.session.context.client.visitorData ?? '')));
- baseURL.textContent = url.toString();
- });
-
- const serializer = new XMLSerializer();
- manifest = serializer.serializeToString(xmlDoc);
-
- dashUri = URL.createObjectURL(new Blob([manifest], { type: 'application/dash+xml;charset=utf8' }));
+ const manifest = await video.toDash();
+ dashUri = `data:application/dash+xml;charset=utf-8;base64,${Buffer.from(manifest).toString('base64')}`;
}
const descString = video.secondary_info.description?.toString() || '';
@@ -199,13 +150,11 @@ export async function patchYoutubeJs(videoId: string): Promise {
});
}
- console.log(video.primary_info);
-
return {
type: 'video',
title: video.primary_info.title?.toString() || '',
- viewCount: Number(video.primary_info.view_count?.original_view_count || 0),
- viewCountText: video.primary_info.view_count?.original_view_count.toString() || '0',
+ viewCount: Number(video.basic_info.view_count || 0),
+ viewCountText: video.basic_info.view_count?.toString() || '0',
likeCount: video.basic_info.like_count || 0,
dislikeCount: 0,
allowRatings: false,
@@ -240,6 +189,7 @@ export async function patchYoutubeJs(videoId: string): Promise {
subCountText: '',
keywords: video.basic_info.keywords || [],
allowedRegions: [],
- fallbackPatch: 'youtubejs'
+ ytJsVideoInfo: video,
+ fallbackPatch: 'youtubejs',
};
}
diff --git a/materialious/src/lib/store.ts b/materialious/src/lib/store.ts
index baa0f87f..186151d1 100644
--- a/materialious/src/lib/store.ts
+++ b/materialious/src/lib/store.ts
@@ -39,7 +39,6 @@ export const playerTheatreModeByDefaultStore = persisted('theatreModeByDefault',
export const playerAutoplayNextByDefaultStore = persisted('autoplayNextByDefault', false);
export const playerYouTubeJsFallback = persisted('youTubeJsFallback', true);
export const playerYouTubeJsAlways = persisted('youTubeJsAlways', false);
-export const playerAndroidBgPlayer = persisted('androidBgPlayer', true);
export const playerAndroidLockOrientation = persisted('androidLockOrientation', true);
export const playerDefaultLanguage = persisted('defaultLanguage', '');
@@ -101,8 +100,7 @@ export const syncPartyConnectionsStore: Writable = writ
export const playlistSettingsStore: Writable> =
writable({});
-export const silenceSkipperStore: Writable = persisted('silenceSkipper', false);
-export const poTokenCacheStore: Writable = writable();
+export const poTokenCacheStore: Writable = writable();
export const searchHistoryStore: Writable = persisted('searchHistory', []);
diff --git a/materialious/src/routes/(app)/+layout.svelte b/materialious/src/routes/(app)/+layout.svelte
index 6d7ec534..769a44df 100644
--- a/materialious/src/routes/(app)/+layout.svelte
+++ b/materialious/src/routes/(app)/+layout.svelte
@@ -12,6 +12,7 @@
import Settings from '$lib/components/Settings/Settings.svelte';
import SyncParty from '$lib/components/SyncParty.svelte';
import Thumbnail from '$lib/components/Thumbnail.svelte';
+ import '$lib/css/global.css';
import { bookmarkletLoadFromUrl, loadSettingsFromEnv } from '$lib/externalSettings';
import { getPages } from '$lib/navPages';
import {
@@ -24,7 +25,6 @@
syncPartyPeerStore,
themeColorStore
} from '$lib/store';
- import '$lib/style.css';
import { setAmoledTheme, setStatusBarColor, setTheme } from '$lib/theme';
import { App } from '@capacitor/app';
import { Browser } from '@capacitor/browser';
diff --git a/materialious/src/routes/(app)/watch/[slug]/+page.svelte b/materialious/src/routes/(app)/watch/[slug]/+page.svelte
index d3dd6e23..390dd08e 100644
--- a/materialious/src/routes/(app)/watch/[slug]/+page.svelte
+++ b/materialious/src/routes/(app)/watch/[slug]/+page.svelte
@@ -1,6 +1,4 @@
-
+
diff --git a/materialious/static/nodejs-android/index.js b/materialious/static/nodejs-android/index.js
index 6cca9c52..f3c41640 100644
--- a/materialious/static/nodejs-android/index.js
+++ b/materialious/static/nodejs-android/index.js
@@ -3,12 +3,17 @@ const https = require('https');
const HOST = 'localhost';
const PORT = 3000;
-const MAX_REDIRECTS = 5;
+const MAX_REDIRECTS = 10;
+
+const CORS_HEADERS = 'Origin, X-Requested-With, Content-Type, Accept, Authorization, x-goog-visitor-id, x-goog-api-key, x-origin, x-youtube-client-version, x-youtube-client-name, x-goog-api-format-version, x-user-agent, Accept-Language, Range, Referer'
+const CORS_ORIGIN = 'https://www.youtube.com'
+const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36(KHTML, like Gecko)'
function setCorsHeaders(res) {
- res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('Access-Control-Allow-Origin', CORS_ORIGIN);
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
- res.setHeader('Access-Control-Allow-Headers', '*');
+ res.setHeader('Access-Control-Allow-Headers', CORS_HEADERS);
+ res.setHeader('Access-Control-Max-Age', '86400')
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
@@ -16,7 +21,6 @@ function fetchWithRedirects(targetUrl, options, redirectCount = 0) {
return new Promise((resolve, reject) => {
const httpClient = targetUrl.protocol.startsWith('https') ? https : http;
-
const req = httpClient.request(targetUrl, options, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
if (redirectCount >= MAX_REDIRECTS) {
@@ -63,7 +67,7 @@ const server = http.createServer(async (req, res) => {
let parsedTarget;
try {
// Ensure protocol (http) is added if missing
- if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) {
+ if (!targetUrl.startsWith('http')) {
targetUrl = 'http://' + targetUrl;
}
parsedTarget = new URL(targetUrl);
@@ -87,6 +91,10 @@ const server = http.createServer(async (req, res) => {
)
};
+ options.headers.host = parsedTarget.host;
+ options.headers.origin = parsedTarget.origin;
+ options.headers['user-agent'] = USER_AGENT;
+
// For POST and PUT methods, pass the body to the outgoing request
if (req.method === 'POST' || req.method === 'PUT') {
options.headers['Content-Length'] = Buffer.byteLength(body);
@@ -103,9 +111,9 @@ const server = http.createServer(async (req, res) => {
res.writeHead(proxyRes.statusCode, {
...proxyRes.headers,
- 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Origin': CORS_ORIGIN,
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
- 'Access-Control-Allow-Headers': '*',
+ 'Access-Control-Allow-Headers': CORS_HEADERS,
'Access-Control-Allow-Credentials': 'true',
});
diff --git a/materialious/tsconfig.json b/materialious/tsconfig.json
index d9a18b12..0fe20b84 100644
--- a/materialious/tsconfig.json
+++ b/materialious/tsconfig.json
@@ -11,7 +11,7 @@
"strict": true,
"moduleResolution": "bundler",
"types": [
- "vidstack/svelte"
+ "shaka-player"
]
},
"include": [
diff --git a/materialious/vite.config.ts b/materialious/vite.config.ts
index 3d713b82..b0246a0d 100644
--- a/materialious/vite.config.ts
+++ b/materialious/vite.config.ts
@@ -1,6 +1,5 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { SvelteKitPWA } from '@vite-pwa/sveltekit';
-import { vite as vidstack } from 'vidstack/plugins';
import { defineConfig } from 'vite';
@@ -34,7 +33,6 @@ export default defineConfig({
name: 'Materialious'
}
}),
- vidstack(),
sveltekit()
],
ssr: {
diff --git a/update_versions.py b/update_versions.py
index 9e32a591..8db7d2ea 100644
--- a/update_versions.py
+++ b/update_versions.py
@@ -3,7 +3,7 @@ import os
import re
from datetime import datetime
-LATEST_VERSION = "1.7.14"
+LATEST_VERSION = "1.7.15"
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")