From 509eea83deeac9825c51fc119f95d3a018553539 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Tue, 4 Nov 2025 22:36:35 +1300 Subject: [PATCH] Improved premiere handling --- materialious/electron/package-lock.json | 4 +- materialious/electron/package.json | 2 +- materialious/package-lock.json | 11 ++++- materialious/package.json | 3 +- materialious/src/lib/api/model.ts | 2 +- materialious/src/lib/components/Player.svelte | 11 +---- materialious/src/lib/components/Search.svelte | 8 ++-- materialious/src/lib/css/global.css | 9 ++++ materialious/src/lib/i18n/index.ts | 14 +++++- materialious/src/lib/i18n/locales/en.json | 2 +- materialious/src/lib/patches/youtubejs.ts | 6 +-- materialious/src/lib/time.ts | 32 ++++++++++++++ materialious/src/lib/watch.ts | 4 -- .../routes/(app)/watch/[slug]/+page.svelte | 43 +++++++++++++++++-- 14 files changed, 119 insertions(+), 32 deletions(-) create mode 100644 materialious/src/lib/time.ts diff --git a/materialious/electron/package-lock.json b/materialious/electron/package-lock.json index 601e6e53..eb3b1080 100644 --- a/materialious/electron/package-lock.json +++ b/materialious/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "Materialious", - "version": "1.10.18", + "version": "1.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "Materialious", - "version": "1.10.18", + "version": "1.11.0", "license": "MIT", "dependencies": { "@capacitor-community/electron": "^5.0.0", diff --git a/materialious/electron/package.json b/materialious/electron/package.json index 04105ab0..75f5d9e9 100644 --- a/materialious/electron/package.json +++ b/materialious/electron/package.json @@ -44,4 +44,4 @@ "capacitor", "electron" ] -} \ No newline at end of file +} diff --git a/materialious/package-lock.json b/materialious/package-lock.json index 4d04842a..97446ac4 100644 --- a/materialious/package-lock.json +++ b/materialious/package-lock.json @@ -1,12 +1,12 @@ { "name": "materialious", - "version": "1.10.18", + "version": "1.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "materialious", - "version": "1.10.18", + "version": "1.11.0", "hasInstallScript": true, "dependencies": { "@capacitor-community/electron": "^5.0.1", @@ -20,6 +20,7 @@ "beercss": "^3.12.11", "bgutils-js": "^3.2.0", "capacitor-nodejs": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.9/capacitor-nodejs.tgz", + "dayjs": "^1.11.19", "fuse.js": "^7.0.0", "googlevideo": "^4.0.4", "he": "^1.2.0", @@ -5235,6 +5236,12 @@ "node": "*" } }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/materialious/package.json b/materialious/package.json index 2e8c5a67..e729968b 100644 --- a/materialious/package.json +++ b/materialious/package.json @@ -53,6 +53,7 @@ "beercss": "^3.12.11", "bgutils-js": "^3.2.0", "capacitor-nodejs": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.9/capacitor-nodejs.tgz", + "dayjs": "^1.11.19", "fuse.js": "^7.0.0", "googlevideo": "^4.0.4", "he": "^1.2.0", @@ -70,4 +71,4 @@ "svelte-persisted-store": "^0.12.0", "youtubei.js": "^16.0.1" } -} \ No newline at end of file +} diff --git a/materialious/src/lib/api/model.ts b/materialious/src/lib/api/model.ts index 3dd8672a..a3cf0683 100644 --- a/materialious/src/lib/api/model.ts +++ b/materialious/src/lib/api/model.ts @@ -38,7 +38,7 @@ export interface Video extends VideoBase { descriptionHtml: string; published: number; publishedText: string; - premiereTimestamp: number; + premiereTimestamp?: number; liveNow: boolean; premium: boolean; isUpcoming: boolean; diff --git a/materialious/src/lib/components/Player.svelte b/materialious/src/lib/components/Player.svelte index dd651b51..c68af0ce 100644 --- a/materialious/src/lib/components/Player.svelte +++ b/materialious/src/lib/components/Player.svelte @@ -863,7 +863,7 @@ {#if showVideoRetry} -
+
{#if $playerYouTubeJsFallback}

{$_('player.youtubeJsLoading')}

@@ -930,13 +930,4 @@ .hide { display: none; } - - .fallback { - height: 30vh; - width: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - } diff --git a/materialious/src/lib/components/Search.svelte b/materialious/src/lib/components/Search.svelte index 0116af20..1a38fb7a 100644 --- a/materialious/src/lib/components/Search.svelte +++ b/materialious/src/lib/components/Search.svelte @@ -11,6 +11,7 @@ searchHistoryStore } from '../store'; import { isVideoID } from '$lib/misc'; + import { resolve } from '$app/paths'; const dispatch = createEventDispatcher(); @@ -38,15 +39,16 @@ } function handleSubmit(event: Event | undefined = undefined) { - if (event) event.preventDefault(); + event?.preventDefault(); const searchTrimed = search.trim(); - if (searchTrimed) return; + if (!searchTrimed) return; if (isVideoID(searchTrimed)) { // Go directly to video if Video ID provided - goto(`/watch/${searchTrimed}`); + goto(resolve('/watch/[videoId]', { videoId: searchTrimed })); + return; } selectedSuggestionIndex = -1; diff --git a/materialious/src/lib/css/global.css b/materialious/src/lib/css/global.css index 5f711192..b10b2b97 100644 --- a/materialious/src/lib/css/global.css +++ b/materialious/src/lib/css/global.css @@ -51,6 +51,15 @@ dialog { padding: 1em !important; } +.video-placeholder { + height: 30vh; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + @media screen and (max-width: 1000px) { menu.mobile { position: fixed !important; diff --git a/materialious/src/lib/i18n/index.ts b/materialious/src/lib/i18n/index.ts index 213a0cbb..b03c0e4f 100644 --- a/materialious/src/lib/i18n/index.ts +++ b/materialious/src/lib/i18n/index.ts @@ -1,6 +1,8 @@ import i18next, { type InitOptions } from 'i18next'; import { writable, type Writable } from 'svelte/store'; +import dayjs from 'dayjs'; + const defaultLocale = 'en'; export const locale: Writable = writable(defaultLocale); @@ -40,6 +42,13 @@ function getUserLocale(): string { return defaultLocale; } +async function loadDayjsLocale(localeName: string) { + try { + const { default: locale } = await import(`dayjs/locale/${localeName}`); + dayjs.locale(locale); + } catch {} +} + export async function initI18n(selectedLocale: string = getUserLocale()): Promise { const langToLoad = resources[selectedLocale] ? selectedLocale : defaultLocale; const translations = await resources[langToLoad](); @@ -63,8 +72,11 @@ export async function initI18n(selectedLocale: string = getUserLocale()): Promis locale.set(langToLoad); _.set(i18next.t.bind(i18next)); - window.addEventListener('languagechange', () => { + await loadDayjsLocale(selectedLocale); + + window.addEventListener('languagechange', async () => { const newLang = getUserLocale(); initI18n(newLang); + await loadDayjsLocale(newLang); }); } diff --git a/materialious/src/lib/i18n/locales/en.json b/materialious/src/lib/i18n/locales/en.json index 60edacb8..aedfc196 100644 --- a/materialious/src/lib/i18n/locales/en.json +++ b/materialious/src/lib/i18n/locales/en.json @@ -2,7 +2,6 @@ "enabled": "Enabled", "disabled": "Disabled", "premium": "Premium YouTube content can't be watched on Materialious.", - "isUpcoming": "Video is not in a watchable state yet.", "copyUrl": "Copy URL", "loadMore": "Load more", "views": "views", @@ -82,6 +81,7 @@ "pauseTimer": "Pause timer", "pauseVideoIn": "Pause video in ", "preferredQuality": "Preferred quality", + "premiere": "Video will be available", "chapters": "Chapters", "share": { "title": "Share", diff --git a/materialious/src/lib/patches/youtubejs.ts b/materialious/src/lib/patches/youtubejs.ts index 341e23e0..9d1e94a2 100644 --- a/materialious/src/lib/patches/youtubejs.ts +++ b/materialious/src/lib/patches/youtubejs.ts @@ -153,8 +153,6 @@ export async function patchYoutubeJs(videoId: string): Promise { return; } - console.log(recommended); - recommendedVideos.push({ videoThumbnails: (recommended?.content_image?.image as Thumbnail[]) || [], videoId: recommended?.content_id || '', @@ -233,7 +231,9 @@ export async function patchYoutubeJs(videoId: string): Promise { descriptionHtml: video.secondary_info.description?.toHTML() || descString, published: 0, publishedText: video.primary_info.published?.toString() || '', - premiereTimestamp: 0, + premiereTimestamp: video.basic_info.is_upcoming + ? Math.floor(video.basic_info.start_timestamp?.getTime() || 0 / 1000) + : undefined, hlsUrl: video.streaming_data?.hls_manifest_url || undefined, liveNow: video.basic_info.is_live || false, // @ts-expect-error Type does have offer_id diff --git a/materialious/src/lib/time.ts b/materialious/src/lib/time.ts new file mode 100644 index 00000000..5c31c56d --- /dev/null +++ b/materialious/src/lib/time.ts @@ -0,0 +1,32 @@ +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import duration from 'dayjs/plugin/duration'; +import relativeTime from 'dayjs/plugin/relativeTime'; +import advancedFormat from 'dayjs/plugin/advancedFormat'; + +dayjs.extend(utc); +dayjs.extend(duration); +dayjs.extend(relativeTime); +dayjs.extend(advancedFormat); + +export function humanFriendlyTimestamp(utcTimestamp: number): string { + const now = dayjs(); + const timestamp = dayjs.utc(utcTimestamp).local(); + + const isSameDay = now.isSame(timestamp, 'day'); + const isSameMonth = now.isSame(timestamp, 'month'); + const isThisYear = now.isSame(timestamp, 'year'); + + const diffMilliseconds = timestamp.diff(now); + const diffDuration = dayjs.duration(diffMilliseconds); + + if (isSameDay) { + return diffDuration.humanize(true); + } else if (isSameMonth) { + return timestamp.format('Do @ h:mm A'); + } else if (isThisYear) { + return timestamp.format('MMMM Do @ h:mm A'); + } else { + return timestamp.format('MMMM Do YYYY h:mm A'); + } +} diff --git a/materialious/src/lib/watch.ts b/materialious/src/lib/watch.ts index d9ff42d8..38299bee 100644 --- a/materialious/src/lib/watch.ts +++ b/materialious/src/lib/watch.ts @@ -30,10 +30,6 @@ export async function getWatchDetails(videoId: string, url: URL) { error(400, get(_)('premium')); } - if (video.isUpcoming) { - error(400, get(_)('isUpcoming')); - } - let personalPlaylists; if (get(authStore)) { postHistory(video.videoId); diff --git a/materialious/src/routes/(app)/watch/[slug]/+page.svelte b/materialious/src/routes/(app)/watch/[slug]/+page.svelte index c2b8c724..499b5c36 100644 --- a/materialious/src/routes/(app)/watch/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/watch/[slug]/+page.svelte @@ -36,6 +36,9 @@ import LikesDislikes from '$lib/components/watch/LikesDislikes.svelte'; import Comment from '$lib/components/watch/Comment.svelte'; import { expandSummery } from '$lib/misc'; + import { humanFriendlyTimestamp } from '$lib/time.js'; + import { getWatchDetails } from '$lib/watch.js'; + import { page } from '$app/state'; let { data = $bindable() } = $props(); @@ -65,6 +68,9 @@ let playerCurrentTime: number = $state(0); + let premiereTime = $state(''); + let premiereUpdateInterval: NodeJS.Timeout; + $effect(() => { if ($interfaceAutoExpandComments && comments) { expandSummery('comment-section'); @@ -231,6 +237,19 @@ playerCurrentTime = playerElement.currentTime; }); } + + if (data.video.premiereTimestamp) { + premiereTime = humanFriendlyTimestamp(data.video.premiereTimestamp); + premiereUpdateInterval = setInterval(async () => { + data = await getWatchDetails(data.video.videoId, page.url); + + if (data.video.premiereTimestamp) { + premiereTime = humanFriendlyTimestamp(data.video.premiereTimestamp); + } else { + clearInterval(premiereUpdateInterval); + } + }, 60000); + } }); onDestroy(() => { @@ -240,6 +259,10 @@ if (pauseTimeout) { clearTimeout(pauseTimeout); } + + if (premiereUpdateInterval) { + clearInterval(premiereUpdateInterval); + } }); async function goToCurrentPlaylistItem() { @@ -319,9 +342,23 @@
- {#key data.video.videoId} - - {/key} + {#if !data.video.premiereTimestamp} + {#key data.video.videoId} + + {/key} + {:else} +
+

{$_('player.premiere')}

+
+ {premiereTime} +
+
+ {/if}
{letterCase(data.video.title)}