diff --git a/materialious/android/app/build.gradle b/materialious/android/app/build.gradle index fe2e5a2e..6478ccfb 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 157 - versionName "1.10.16" + versionCode 158 + versionName "1.10.17" 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/materialious.metainfo.xml b/materialious/electron/materialious.metainfo.xml index 723575bb..9433bd77 100644 --- a/materialious/electron/materialious.metainfo.xml +++ b/materialious/electron/materialious.metainfo.xml @@ -67,6 +67,10 @@ intense + + + https://github.com/Materialious/Materialious/releases/tag/1.10.17 + https://github.com/Materialious/Materialious/releases/tag/1.10.16 diff --git a/materialious/electron/package-lock.json b/materialious/electron/package-lock.json index f79227fd..1aa551a7 100644 --- a/materialious/electron/package-lock.json +++ b/materialious/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "Materialious", - "version": "1.10.16", + "version": "1.10.17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "Materialious", - "version": "1.10.16", + "version": "1.10.17", "license": "MIT", "dependencies": { "@capacitor-community/electron": "^5.0.0", diff --git a/materialious/electron/package.json b/materialious/electron/package.json index 25ed2fd5..00724da4 100644 --- a/materialious/electron/package.json +++ b/materialious/electron/package.json @@ -1,6 +1,6 @@ { "name": "Materialious", - "version": "1.10.16", + "version": "1.10.17", "description": "Modern material design for Invidious.", "author": { "name": "Ward Pearce", diff --git a/materialious/package-lock.json b/materialious/package-lock.json index 3d30b965..777e2f4b 100644 --- a/materialious/package-lock.json +++ b/materialious/package-lock.json @@ -1,12 +1,12 @@ { "name": "materialious", - "version": "1.10.15", + "version": "1.10.17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "materialious", - "version": "1.10.15", + "version": "1.10.17", "hasInstallScript": true, "dependencies": { "@capacitor-community/electron": "^5.0.1", @@ -35,7 +35,7 @@ "sponsorblock-api": "^0.2.4", "svelte-infinite-loading": "^1.4.0", "svelte-persisted-store": "^0.12.0", - "youtubei.js": "^15.1.1" + "youtubei.js": "^16.0.0" }, "devDependencies": { "@capacitor/assets": "^3.0.5", @@ -7988,18 +7988,6 @@ "node": ">=10" } }, - "node_modules/jintr": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jintr/-/jintr-3.3.1.tgz", - "integrity": "sha512-nnOzyhf0SLpbWuZ270Omwbj5LcXUkTcZkVnK8/veJXtSZOiATM5gMZMdmzN75FmTyj+NVgrGaPdH12zIJ24oIA==", - "funding": [ - "https://github.com/sponsors/LuanRT" - ], - "license": "MIT", - "dependencies": { - "acorn": "^8.8.0" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -8667,6 +8655,15 @@ "node": ">=0.6.0" } }, + "node_modules/meriyah": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/meriyah/-/meriyah-6.1.4.tgz", + "integrity": "sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==", + "license": "ISC", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -12330,15 +12327,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/undici": { - "version": "6.21.3", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", - "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -14014,17 +14002,16 @@ } }, "node_modules/youtubei.js": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/youtubei.js/-/youtubei.js-15.1.1.tgz", - "integrity": "sha512-fuEDj9Ky6cAQg93BrRVCbr+GTYNZQAIFZrx/a3oDRuGc3Mf5bS0dQfoYwwgjtSV7sgAKQEEdGtzRdBzOc8g72Q==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/youtubei.js/-/youtubei.js-16.0.0.tgz", + "integrity": "sha512-aMx+ulnrxzsgVsxTr7gbBVnIjti2NQUlMwCoo1/MzICCJS3iMLOPUFdo7bSpwskL6ljzQ/LxmmB4WQC3FtkBlA==", "funding": [ "https://github.com/sponsors/LuanRT" ], "license": "MIT", "dependencies": { "@bufbuild/protobuf": "^2.0.0", - "jintr": "^3.3.1", - "undici": "^6.21.3" + "meriyah": "^6.1.4" } }, "node_modules/zimmerframe": { diff --git a/materialious/package.json b/materialious/package.json index 887c6b4d..3cffa872 100644 --- a/materialious/package.json +++ b/materialious/package.json @@ -1,6 +1,6 @@ { "name": "materialious", - "version": "1.10.16", + "version": "1.10.17", "private": true, "scripts": { "dev": "vite dev", @@ -68,6 +68,6 @@ "sponsorblock-api": "^0.2.4", "svelte-infinite-loading": "^1.4.0", "svelte-persisted-store": "^0.12.0", - "youtubei.js": "^15.1.1" + "youtubei.js": "^16.0.0" } -} \ No newline at end of file +} diff --git a/materialious/src/lib/components/Player.svelte b/materialious/src/lib/components/Player.svelte index be69b2c6..dd651b51 100644 --- a/materialious/src/lib/components/Player.svelte +++ b/materialious/src/lib/components/Player.svelte @@ -36,7 +36,6 @@ playerStatisticsByDefault, playerYouTubeJsFallback, playlistSettingsStore, - poTokenCacheStore, sponsorBlockCategoriesStore, sponsorBlockDisplayToastStore, sponsorBlockStore, @@ -277,10 +276,24 @@ if (isVisible) { player.setTextTrackVisibility(false); } else { - const defaultLanguage = get(playerDefaultLanguage); - const langCode = ISO6391.getCode(defaultLanguage); + let langCode: string; + if ($playerDefaultLanguage === 'original') { + const languageAndRole = player + .getAudioLanguagesAndRoles() + .find(({ role }) => role === 'main'); + + if (!languageAndRole) { + return; + } + + langCode = languageAndRole.language; + } else { + const defaultLanguage = get(playerDefaultLanguage); + langCode = ISO6391.getCode(defaultLanguage); + } const tracks = player.getTextTracks(); + const subtitleTrack = tracks.find((track) => track.language === langCode); if (subtitleTrack) { @@ -295,7 +308,7 @@ async function loadVideo() { showVideoRetry = false; - sabrAdapter = injectSabr(data.video, player); + sabrAdapter = await injectSabr(data.video, player); try { document.getElementsByClassName('shaka-ad-info')[0].remove(); @@ -340,8 +353,8 @@ } if (data.video.captions) { - data.video.captions.forEach(async (caption) => { - player.addTextTrackAsync( + for (const caption of data.video.captions) { + await player.addTextTrackAsync( caption.url.startsWith('http') ? caption.url : `${get(instanceStore)}${caption.url}`, caption.language_code, 'captions', @@ -349,7 +362,7 @@ undefined, caption.label ); - }); + } } if (data.content.timestamps) { @@ -495,6 +508,33 @@ console.error('Player error:', error); }); + player.getNetworkingEngine()?.registerResponseFilter((type, response, _) => { + if ( + type !== shaka.net.NetworkingEngine.RequestType.SEGMENT || + !response.uri.includes('/api/timedtext') + ) { + return; + } + + const url = new URL(response.uri); + + // Fix positioning for auto-generated subtitles + // Credit to Freetube! + if ( + url.hostname.endsWith('.youtube.com') && + url.pathname === '/api/timedtext' && + url.searchParams.get('caps') === 'asr' && + url.searchParams.get('kind') === 'asr' && + url.searchParams.get('fmt') === 'vtt' + ) { + const stringBody = new TextDecoder().decode(response.data); + // position:0% for LTR text and position:100% for RTL text + const cleaned = stringBody.replaceAll(/ align:start position:(?:10)?0%$/gm, ''); + // @ts-expect-error Type is acceptable, is shaka player + response.data = new TextEncoder().encode(cleaned).buffer; + } + }); + // Required to stop buttons from being still selected when fullscreening document?.addEventListener('fullscreenchange', async () => { const buttons = document.querySelectorAll('.shaka-controls-button-panel button'); diff --git a/materialious/src/lib/components/Search.svelte b/materialious/src/lib/components/Search.svelte index d84955a9..3c22de02 100644 --- a/materialious/src/lib/components/Search.svelte +++ b/materialious/src/lib/components/Search.svelte @@ -147,30 +147,32 @@ {#if searchSuggestions} {#each suggestionsForSearch as suggestion, index} - { - search = suggestion; - handleSubmit(); - }} - class="row" - class:selected={index === selectedSuggestionIndex} - href={`/search/${encodeURIComponent(suggestion)}`} - > -
{suggestion}
-
+
  • + { + search = suggestion; + handleSubmit(); + }} + class:selected={index === selectedSuggestionIndex} + href={`/search/${encodeURIComponent(suggestion)}`} + > +
    {suggestion}
    +
    +
  • {/each} {/if} {#if !suggestionsForSearch.length && $interfaceSearchHistoryEnabled} {#each $searchHistoryStore as history} - { - search = history; - }} - href={`/search/${encodeURIComponent(history)}`} - > -
    {history}
    -
    +
  • + { + search = history; + }} + href={`/search/${encodeURIComponent(history)}`} + > +
    {history}
    +
    +
  • {/each} {/if} diff --git a/materialious/src/lib/components/Transcript.svelte b/materialious/src/lib/components/Transcript.svelte index 44d5501b..2de61aaa 100644 --- a/materialious/src/lib/components/Transcript.svelte +++ b/materialious/src/lib/components/Transcript.svelte @@ -92,7 +92,7 @@ } -
    +
    {$_('transcript')}
    diff --git a/materialious/src/lib/components/Watch/Description.svelte b/materialious/src/lib/components/Watch/Description.svelte index 4cc6ffee..318132bf 100644 --- a/materialious/src/lib/components/Watch/Description.svelte +++ b/materialious/src/lib/components/Watch/Description.svelte @@ -30,14 +30,14 @@ {@html description}
    -
    - +
    + {/if} diff --git a/materialious/src/lib/css/global.css b/materialious/src/lib/css/global.css index 6ffa26b7..5f711192 100644 --- a/materialious/src/lib/css/global.css +++ b/materialious/src/lib/css/global.css @@ -1,3 +1,12 @@ +::-webkit-scrollbar { + display: none; +} + +@font-face { + font-family: Inter; + src: url('/static/inter.ttf'); +} + article { box-shadow: none !important; } diff --git a/materialious/src/lib/css/shaka-player-theme.css b/materialious/src/lib/css/shaka-player-theme.css index 6c056c3e..75feabaf 100644 --- a/materialious/src/lib/css/shaka-player-theme.css +++ b/materialious/src/lib/css/shaka-player-theme.css @@ -1,3 +1,7 @@ +.shaka-text-container > div { + align-items: center !important; +} + .player-theme { font-family: var(--font) !important; color: var(--on-surface) !important; @@ -18,7 +22,7 @@ visibility: visible !important; z-index: 99999999 !important; pointer-events: auto !important; - max-height: 400px !important; + max-height: 400px; } .shaka-overflow-menu button, @@ -97,3 +101,15 @@ visibility: visible !important; } } + +@media only screen and (width <=400px) { + :deep(.shaka-text-container) { + font-size: 16px !important; + } + + .player-theme .shaka-overflow-menu, + .player-theme .shaka-settings-menu { + max-height: 80%; + overflow-y: scroll; + } +} diff --git a/materialious/src/lib/patches/youtubejs.ts b/materialious/src/lib/patches/youtubejs.ts index ebe57152..479e3b51 100644 --- a/materialious/src/lib/patches/youtubejs.ts +++ b/materialious/src/lib/patches/youtubejs.ts @@ -1,6 +1,7 @@ import { androidPoTokenMinter } from '$lib/android/youtube/minter'; import type { AdaptiveFormats, + Captions, Image, StoryBoard, Thumbnail, @@ -11,7 +12,27 @@ import { interfaceRegionStore, poTokenCacheStore } from '$lib/store'; import { Capacitor } from '@capacitor/core'; import BG, { USER_AGENT } from 'bgutils-js'; import { get } from 'svelte/store'; -import { Innertube, UniversalCache, Utils, YT, YTNodes } from 'youtubei.js'; +import type { Types } from 'youtubei.js'; +import { Innertube, UniversalCache, Utils, YT, YTNodes, Platform } from 'youtubei.js'; + +Platform.shim.eval = async ( + data: Types.BuildScriptResult, + env: Record +) => { + const properties = []; + + if (env.n) { + properties.push(`n: exportedVars.nFunction("${env.n}")`); + } + + if (env.sig) { + properties.push(`sig: exportedVars.sigFunction("${env.sig}")`); + } + + const code = `${data.output}\nreturn { ${properties.join(', ')} }`; + + return new Function(code)(); +}; export async function patchYoutubeJs(videoId: string): Promise { if (!Capacitor.isNativePlatform()) { @@ -22,8 +43,7 @@ export async function patchYoutubeJs(videoId: string): Promise { fetch: fetch, cache: new UniversalCache(false), location: get(interfaceRegionStore), - user_agent: USER_AGENT, - player_id: '0004de42' + user_agent: USER_AGENT }); const requestKey = 'O43z0dpjhgX20SCx4KAo'; @@ -44,7 +64,7 @@ export async function patchYoutubeJs(videoId: string): Promise { pyv: true }, contentPlaybackContext: { - signatureTimestamp: youtube.session.player?.sts + signatureTimestamp: youtube.session.player?.signature_timestamp } } }); @@ -61,13 +81,7 @@ export async function patchYoutubeJs(videoId: string): Promise { throw new Error('Unable to pull video info from youtube.js'); } - if (video.basic_info.is_live) { - poTokenCacheStore.set(await platformMinter(requestKey, videoId)); - } else { - poTokenCacheStore.set(BG.PoToken.generateColdStartToken(videoId)); - - platformMinter(requestKey, videoId).then((poToken) => poTokenCacheStore.set(poToken)); - } + poTokenCacheStore.set(await platformMinter(requestKey, videoId)); let dashUri: string | undefined; @@ -90,7 +104,6 @@ export async function patchYoutubeJs(videoId: string): Promise { await video.toDash({ manifest_options: { is_sabr: true, - captions_format: 'vtt', include_thumbnails: false } }) @@ -108,6 +121,27 @@ export async function patchYoutubeJs(videoId: string): Promise { authorThumbnails = []; } + const captions: Captions[] = []; + video.captions?.caption_tracks?.forEach((caption) => { + const url = new URL(caption.base_url); + + url.searchParams.set('potc', '1'); + url.searchParams.set('pot', get(poTokenCacheStore) ?? ''); + url.searchParams.set('c', youtube.session.context.client.clientName); + url.searchParams.set('fmt', 'vtt'); + + // Remove &xosf=1 as it adds `position:63% line:0%` to the subtitle lines + // placing them in the top right corner + url.searchParams.delete('xosf'); + + captions.push({ + label: caption.name?.toString() || '', + language_code: caption.language_code, + // Add correct format to url. + url: url.toString() + }); + }); + const recommendedVideos: VideoBase[] = []; video.watch_next_feed?.forEach((recommended: Record) => { if ( @@ -189,7 +223,7 @@ export async function patchYoutubeJs(videoId: string): Promise { formatStreams: [], recommendedVideos: recommendedVideos, authorThumbnails: authorThumbnails, - captions: [], + captions: captions, authorId: video.basic_info.channel_id || '', authorUrl: `/channel/${video.basic_info.channel_id}`, authorVerified: false, diff --git a/materialious/src/lib/sabr/index.ts b/materialious/src/lib/sabr/index.ts index 05fc5f32..c64946ac 100644 --- a/materialious/src/lib/sabr/index.ts +++ b/materialious/src/lib/sabr/index.ts @@ -8,7 +8,10 @@ import { get } from 'svelte/store'; import { poTokenCacheStore } from '$lib/store'; import { buildSabrFormat } from 'googlevideo/utils'; -export function injectSabr(video: VideoPlay, player: shaka.Player): SabrStreamingAdapter | null { +export async function injectSabr( + video: VideoPlay, + player: shaka.Player +): Promise { if (!video.ytjs || !Capacitor.isNativePlatform()) return null; const sabrAdapter = new SabrStreamingAdapter({ @@ -44,7 +47,7 @@ export function injectSabr(video: VideoPlay, player: shaka.Player): SabrStreamin pyv: true }, contentPlaybackContext: { - signatureTimestamp: video.ytjs.innertube.session.player?.sts + signatureTimestamp: video.ytjs.innertube.session.player?.signature_timestamp }, reloadPlaybackContext: reloadContext } @@ -56,7 +59,7 @@ export function injectSabr(video: VideoPlay, player: shaka.Player): SabrStreamin video.ytjs.video.cpn ); sabrAdapter.setStreamingURL( - video.ytjs.innertube.session.player!.decipher( + await video.ytjs.innertube.session.player!.decipher( parsedInfo.streaming_data?.server_abr_streaming_url ) ); @@ -77,7 +80,7 @@ export function injectSabr(video: VideoPlay, player: shaka.Player): SabrStreamin if (video.ytjs.video.streaming_data && !isPostLiveDVR && !isLive) { sabrAdapter.setStreamingURL( - video.ytjs.innertube.session.player!.decipher( + await video.ytjs.innertube.session.player!.decipher( video.ytjs.video.streaming_data?.server_abr_streaming_url ) ); diff --git a/materialious/src/routes/(app)/playlist/[slug]/+page.svelte b/materialious/src/routes/(app)/playlist/[slug]/+page.svelte index 2c7fe5be..4918015b 100644 --- a/materialious/src/routes/(app)/playlist/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/playlist/[slug]/+page.svelte @@ -90,6 +90,8 @@ {/if}
    +
    + {#if data.playlist.videos}