Merge pull request #1002 from Materialious/update/1.9.18

Improved player position resuming and fix chapters on TV
This commit is contained in:
Ward
2025-06-25 02:31:23 +12:00
committed by GitHub
18 changed files with 118 additions and 78 deletions
+2 -2
View File
@@ -5,8 +5,8 @@ on:
branches: [main]
paths-ignore:
- "**/*.md"
- "fastline/**"
- "materialious/src/i18n/locales/**"
- "./fastline/**"
- "./materialious/src/lib/i18n/locales/**"
workflow_dispatch:
+2 -2
View File
@@ -5,8 +5,8 @@ on:
branches: [main]
paths-ignore:
- "**/*.md"
- "fastline/**"
- "materialious/src/i18n/locales/**"
- "./fastline/**"
- "./materialious/src/lib/i18n/locales/**"
workflow_dispatch:
+2 -2
View File
@@ -6,8 +6,8 @@ on:
- main
paths-ignore:
- "**/*.md"
- "fastline/**"
- "materialious/src/i18n/locales/**"
- "./fastline/**"
- "./materialious/src/lib/i18n/locales/**"
jobs:
web-build:
+3 -3
View File
@@ -234,13 +234,13 @@ Help [translate Materialious via Weblate](https://toolate.othing.xyz/projects/ma
# Special thanks to
- [SecularSteve](https://github.com/SecularSteve) for creating Materialious' logo.
- [Invidious](https://github.com/iv-org)
- [Clipious](https://github.com/lamarios/clipious) for inspiration & a good source for learning more about undocumented Invidious routes.
- [Invidious](https://github.com/iv-org).
- [Clipious](https://github.com/lamarios/clipious) for inspiration & references to undocumented Invidious routes.
- [Beer CSS](https://github.com/beercss/beercss) (Especially the [YouTube template](https://github.com/beercss/beercss/tree/main/src/youtube) what was used as the base for Materialious.)
- Every dependency in [package.json](/materialious/package.json).
- [LuanRT](https://github.com/LuanRT) for YouTube.js & SABR implementation.
- [FreeTube](https://github.com/FreeTubeApp/FreeTube) for their subtitle fix.
- [spotube](https://github.com/KRTirtho/spotube) for using their README layout for local installation.
- Every dependency in [package.json](/materialious/package.json).
# Developers
- [How Materialious is built](./docs/BUILDING.md)
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "us.materialio.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 134
versionName "1.9.17"
versionCode 135
versionName "1.9.18"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -69,7 +69,11 @@
<release version="1.9.17" date="2025-6-25">
<release version="1.9.18" date="2025-6-25">
<url>https://github.com/Materialious/Materialious/releases/tag/1.9.18</url>
</release>
<release version="1.9.17" date="2025-6-25">
<url>https://github.com/Materialious/Materialious/releases/tag/1.9.17</url>
</release>
<release version="1.9.16" date="2025-6-24">
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "Materialious",
"version": "1.9.17",
"version": "1.9.18",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "Materialious",
"version": "1.9.17",
"version": "1.9.18",
"license": "MIT",
"dependencies": {
"@capacitor-community/electron": "^5.0.0",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "Materialious",
"version": "1.9.17",
"version": "1.9.18",
"description": "Modern material design for Invidious.",
"author": {
"name": "Ward Pearce",
@@ -44,4 +44,4 @@
"capacitor",
"electron"
]
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "materialious",
"version": "1.9.17",
"version": "1.9.18",
"private": true,
"scripts": {
"dev": "vite dev",
+9 -4
View File
@@ -420,13 +420,18 @@ export async function getThumbnail(
return URL.createObjectURL(await resp.blob());
}
function buildApiExtendedAuthHeaders(): Record<string, Record<string, string>> {
const authToken = get(authStore)?.token ?? '';
return { headers: { Authorization: `Bearer ${authToken.replace('SID=', '')}` } };
}
export async function getVideoProgress(
videoId: string,
fetchOptions: RequestInit = {}
): Promise<SynciousProgressModel[]> {
const resp = await fetchErrorHandle(
await fetch(`${get(synciousInstanceStore)}/video/${encodeURIComponent(videoId)}`, {
...buildAuthHeaders(),
...buildApiExtendedAuthHeaders(),
...fetchOptions
})
);
@@ -439,7 +444,7 @@ export async function saveVideoProgress(
time: number,
fetchOptions: RequestInit = {}
) {
const headers: Record<string, Record<string, string>> = buildAuthHeaders();
const headers: Record<string, Record<string, string>> = buildApiExtendedAuthHeaders();
headers['headers']['Content-type'] = 'application/json';
await fetchErrorHandle(
@@ -458,7 +463,7 @@ export async function deleteVideoProgress(videoId: string, fetchOptions: Request
await fetchErrorHandle(
await fetch(`${get(synciousInstanceStore)}/video/${videoId}`, {
method: 'DELETE',
...buildAuthHeaders(),
...buildApiExtendedAuthHeaders(),
...fetchOptions
})
);
@@ -468,7 +473,7 @@ export async function deleteAllVideoProgress(fetchOptions: RequestInit = {}) {
await fetchErrorHandle(
await fetch(`${get(synciousInstanceStore)}/videos`, {
method: 'DELETE',
...buildAuthHeaders(),
...buildApiExtendedAuthHeaders(),
...fetchOptions
})
);
+38 -25
View File
@@ -29,6 +29,7 @@
playerAndroidLockOrientation,
playerAutoplayNextByDefaultStore,
playerAutoPlayStore,
playerCCByDefault,
playerDefaultLanguage,
playerDefaultPlaybackSpeed,
playerDefaultQualityStore,
@@ -313,6 +314,24 @@
return false;
}
function toggleSubtitles() {
const isVisible = player.isTextTrackVisible();
if (isVisible) {
player.setTextTrackVisibility(false);
} else {
const defaultLanguage = get(playerDefaultLanguage);
const langCode = ISO6391.getCode(defaultLanguage);
const tracks = player.getTextTracks();
const subtitleTrack = tracks.find((track) => track.language === langCode);
if (subtitleTrack) {
player.selectTextTrack(subtitleTrack);
player.setTextTrackVisibility(true);
}
}
}
page.subscribe((pageUpdate) => loadTimeFromUrl(pageUpdate));
async function loadVideo() {
@@ -359,6 +378,9 @@
}
}
if (watchProgressTimeout) {
clearInterval(watchProgressTimeout);
}
// Auto save watch progress every minute.
watchProgressTimeout = setInterval(() => savePlayerPos(), 60000);
setupSponsorSkip();
@@ -378,9 +400,7 @@
dashUrl += '?local=true';
}
await player.load(dashUrl);
await loadPlayerPos();
await player.load(dashUrl, await getLastPlayPos());
} else {
if (data.video.fallbackPatch === 'youtubejs') {
await player.load(data.video.dashUrl);
@@ -396,6 +416,9 @@
restoreQualityPreference();
restoreDefaultLanguage();
if ($playerCCByDefault) {
toggleSubtitles();
}
if ($playerDefaultPlaybackSpeed && playerElement) {
playerElement.playbackRate = $playerDefaultPlaybackSpeed;
@@ -541,21 +564,7 @@
}
Mousetrap.bind('c', () => {
const isVisible = player.isTextTrackVisible();
if (isVisible) {
player.setTextTrackVisibility(false);
} else {
const defaultLanguage = get(playerDefaultLanguage);
const langCode = ISO6391.getCode(defaultLanguage);
const tracks = player.getTextTracks();
const subtitleTrack = tracks.find((track) => track.language === langCode);
if (subtitleTrack) {
player.selectTextTrack(subtitleTrack);
player.setTextTrackVisibility(true);
}
}
toggleSubtitles();
return false;
});
@@ -582,6 +591,10 @@
return false;
});
playerElement.addEventListener('pause', async () => {
savePlayerPos();
});
playerElement.addEventListener('ended', async () => {
if (!data.playlistId) {
if ($playerAutoplayNextByDefaultStore) {
@@ -650,8 +663,8 @@
}
});
async function loadPlayerPos() {
if (loadTimeFromUrl($page)) return;
async function getLastPlayPos(): Promise<number> {
if (loadTimeFromUrl($page)) return 0;
let toSetTime = 0;
@@ -670,7 +683,7 @@
}
}
if (toSetTime > 0 && playerElement) playerElement.currentTime = toSetTime;
return toSetTime;
}
function savePlayerPos() {
@@ -713,16 +726,16 @@
}
}
try {
savePlayerPos();
} catch (error) {}
Mousetrap.unbind(['left', 'right', 'space', 'c', 'f', 'shift+left', 'shift+right']);
if (watchProgressTimeout) {
clearTimeout(watchProgressTimeout);
}
try {
savePlayerPos();
} catch (error) {}
HttpFetchPlugin.cacheManager.clearCache();
if (playerElementResizeObserver) {
@@ -9,6 +9,7 @@
playerAndroidLockOrientation,
playerAutoPlayStore,
playerAutoplayNextByDefaultStore,
playerCCByDefault,
playerDefaultLanguage,
playerDefaultPlaybackSpeed,
playerDefaultQualityStore,
@@ -133,6 +134,22 @@
</div>
{/if}
<div class="field no-margin">
<nav class="no-padding">
<div class="max">
<div>{$_('layout.player.ccByDefault')}</div>
</div>
<label class="switch" tabindex="0" role="switch">
<input
type="checkbox"
bind:checked={$playerCCByDefault}
onclick={() => playerCCByDefault.set(!$playerCCByDefault)}
/>
<span></span>
</label>
</nav>
</div>
<div class="field no-margin">
<nav class="no-padding">
<div class="max">
+8 -2
View File
@@ -34,7 +34,8 @@ import {
themeColorStore,
interfaceAutoExpandChapters,
playerDefaultPlaybackSpeed,
playerStatisticsByDefault
playerStatisticsByDefault,
playerCCByDefault
} from '$lib/store';
import { get, type Writable } from 'svelte/store';
@@ -217,6 +218,11 @@ const persistedStores: {
name: 'playerStatistics',
store: playerStatisticsByDefault,
type: 'boolean'
},
{
name: 'CCByDefault',
store: playerCCByDefault,
type: 'boolean'
}
];
@@ -227,7 +233,7 @@ function setStores(toSet: Record<string, any>) {
userOverwritten = localStorage.getItem(store.name) !== null;
} catch {}
let paramValue = toSet[store.name];
const paramValue = toSet[store.name];
if (typeof paramValue !== 'undefined' && !userOverwritten) {
let value: any;
@@ -144,6 +144,7 @@
"alwaysLoopVideo": "Always loop video",
"savePlaybackPosition": "Save playback position",
"listenByDefault": "Listen by default",
"ccByDefault": "Subtitles on by default",
"theatreModeByDefault": "Theatre mode by default",
"autoPlayNextByDefault": "Autoplay next by default",
"miniPlayer": "Mini player",
+2 -12
View File
@@ -98,7 +98,7 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
? video.streaming_data.hls_manifest_url // HLS is preferred for DVR streams.
: `${video.streaming_data.dash_manifest_url}/mpd_version/7`;
} else {
dashUri = `data:application/dash+xml;base64,${btoa(await video.toDash(undefined))}`;
dashUri = `data:application/dash+xml;base64,${btoa(await video.toDash({ captions_format: 'vtt' }))}`;
}
}
@@ -133,16 +133,6 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
});
});
const captions: Captions[] = [];
video.captions?.caption_tracks?.forEach((caption) => {
captions.push({
label: caption.name?.toString() || '',
language_code: caption.language_code,
// Add correct format to url.
url: caption.base_url + '&fmt=vtt'
});
});
const adaptiveFormats: AdaptiveFormats[] = [];
video.streaming_data?.adaptive_formats.forEach((format) => {
adaptiveFormats.push({
@@ -194,7 +184,7 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
formatStreams: [],
recommendedVideos: recommendedVideos,
authorThumbnails: authorThumbnails,
captions: captions,
captions: [],
authorId: video.basic_info.channel_id || '',
authorUrl: `/channel/${video.basic_info.channel_id}`,
authorVerified: false,
+2 -1
View File
@@ -53,7 +53,8 @@ export const playerAutoplayNextByDefaultStore = persisted('autoplayNextByDefault
export const playerYouTubeJsFallback = persisted('youTubeJsFallback', true);
export const playerYouTubeJsAlways = persisted('youTubeJsAlways', false);
export const playerAndroidLockOrientation = persisted('androidLockOrientation', true);
export const playerDefaultLanguage = persisted('defaultLanguage', '');
export const playerDefaultLanguage = persisted('defaultLanguage', 'english');
export const playerCCByDefault = persisted('CCByDefault', false);
export const playerDefaultPlaybackSpeed: Writable<number> = persisted('defaultPlaybackSpeed', 1);
export const playerStatisticsByDefault = persisted('playerStatistics', false);
@@ -111,22 +111,25 @@
<h5 style="margin-bottom: 0;">{$_('player.chapters')}</h5>
<div class="grid">
{#each data.content.timestamps as timestamp}
<article
role="presentation"
style="cursor: pointer;"
onclick={() => {
if (playerElement) playerElement.currentTime = timestamp.time;
}}
>
<div style="white-space: pre-line; overflow-wrap: break-word;text-align: center;">
<p style="no-margin no-padding">{timestamp.title}</p>
<span
class:primary={playerCurrentTime >= timestamp.time &&
(playerCurrentTime <= timestamp.endTime || timestamp.endTime === -1)}
class="chip no-margin">{timestamp.timePretty}</span
>
</div>
</article>
<ContentColumn>
<article
role="presentation"
style="cursor: pointer;height: 100%;"
onclick={() => {
if (playerElement) playerElement.currentTime = timestamp.time;
showInfo = false;
}}
>
<div style="white-space: pre-line; overflow-wrap: break-word;text-align: center;">
<p style="no-margin no-padding">{timestamp.title}</p>
<span
class:primary={playerCurrentTime >= timestamp.time &&
(playerCurrentTime <= timestamp.endTime || timestamp.endTime === -1)}
class="chip no-margin">{timestamp.timePretty}</span
>
</div>
</article>
</ContentColumn>
{/each}
</div>
{/if}
+1 -1
View File
@@ -3,7 +3,7 @@ import os
import re
from datetime import datetime
LATEST_VERSION = "1.9.17"
LATEST_VERSION = "1.9.18"
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")