From 0e1a2f30f3a031caf65b427a5fd7c6de4415d587 Mon Sep 17 00:00:00 2001 From: ward Date: Mon, 23 Jun 2025 12:25:33 +1200 Subject: [PATCH] Implemented batch loading for API Extended --- materialious/src/lib/api/apiExtended.ts | 59 +++++++++++++++ materialious/src/lib/api/index.ts | 2 +- materialious/src/lib/components/Player.svelte | 30 ++++---- .../src/lib/components/Thumbnail.svelte | 5 +- materialious/src/lib/watch.ts | 71 +++++++++++++++++++ .../src/routes/(app)/watch/[slug]/+page.ts | 70 +----------------- .../src/routes/(no-layout)/tv/[slug]/+page.ts | 23 +----- 7 files changed, 153 insertions(+), 107 deletions(-) create mode 100644 materialious/src/lib/api/apiExtended.ts create mode 100644 materialious/src/lib/watch.ts diff --git a/materialious/src/lib/api/apiExtended.ts b/materialious/src/lib/api/apiExtended.ts new file mode 100644 index 00000000..ec321213 --- /dev/null +++ b/materialious/src/lib/api/apiExtended.ts @@ -0,0 +1,59 @@ +// src/lib/requestQueue.ts +import { writable, type Writable } from 'svelte/store'; +import type { SynciousProgressModel } from './model'; +import { getVideoProgress } from '.'; + +const synciousCacheStore: Writable = writable(null); + +const videoIds: string[] = []; +const pendingResolves = new Map void>(); + +let timeout: ReturnType | null = null; +const DEBOUNCE_MS = 1000; +const BATCH_SIZE = 100; + +async function processBatches(): Promise { + const batches: string[][] = []; + + while (videoIds.length > 0) { + batches.push(videoIds.splice(0, BATCH_SIZE)); + } + + const results: SynciousProgressModel[] = []; + + for (const batch of batches) { + const res: SynciousProgressModel[] = await getVideoProgress(batch.join(',')); + results.push(...res); + + // Resolve pending promises for this batch + for (const videoId of batch) { + const match = res.find((item) => item.video_id === videoId); + const resolve = pendingResolves.get(videoId); + if (resolve) { + resolve(match); + pendingResolves.delete(videoId); + } + } + } + + synciousCacheStore.set(results); +} + +export function queueSyncious(videoId: string): Promise { + videoIds.push(videoId); + + const promise = new Promise((resolve) => { + pendingResolves.set(videoId, resolve); + }); + + if (timeout) clearTimeout(timeout); + timeout = setTimeout(() => { + processBatches().catch((e) => { + console.error('Failed to process batches:', e); + }); + }, DEBOUNCE_MS); + + return promise; +} + +export { synciousCacheStore }; diff --git a/materialious/src/lib/api/index.ts b/materialious/src/lib/api/index.ts index 39440178..d9d80f98 100644 --- a/materialious/src/lib/api/index.ts +++ b/materialious/src/lib/api/index.ts @@ -64,7 +64,7 @@ export function buildAuthHeaders(): { headers: Record } { if (authToken.startsWith('SID=')) { return { headers: { __sid_auth: authToken } }; } else { - return { headers: { Authorization: `Bearer ${get(authStore)?.token}` } }; + return { headers: { Authorization: `Bearer ${authToken}` } }; } } diff --git a/materialious/src/lib/components/Player.svelte b/materialious/src/lib/components/Player.svelte index 538ed8c6..9e554104 100644 --- a/materialious/src/lib/components/Player.svelte +++ b/materialious/src/lib/components/Player.svelte @@ -90,7 +90,18 @@ played: (await getDynamicTheme())['--primary'] } }); + setChapterMarkers(); + + const overflowMenuButton = document.querySelector('.shaka-overflow-menu-button'); + if (overflowMenuButton) { + overflowMenuButton.innerHTML = 'settings'; + } + + const backToOverflowButton = document.querySelector('.shaka-back-to-overflow-button'); + if (backToOverflowButton) { + backToOverflowButton.innerHTML = 'arrow_back_ios_new'; + } } themeColorStore.subscribe(updateSeekBarTheme); @@ -477,12 +488,11 @@ 'statistics' ], playbackRates: playbackRates, - enableTooltips: false, - seekBarColors: { - played: (await getDynamicTheme())['--primary'] - } + enableTooltips: false }); + updateSeekBarTheme(); + player.addEventListener('error', async (event) => { const error = (event as CustomEvent).detail as shaka.util.Error; console.error('Player error:', error); @@ -504,16 +514,6 @@ await androidHandleRotate(); - const overflowMenuButton = document.querySelector('.shaka-overflow-menu-button'); - if (overflowMenuButton) { - overflowMenuButton.innerHTML = 'settings'; - } - - const backToOverflowButton = document.querySelector('.shaka-back-to-overflow-button'); - if (backToOverflowButton) { - backToOverflowButton.innerHTML = 'arrow_back_ios_new'; - } - Mousetrap.bind('space', () => { if (!playerElement) return; @@ -580,8 +580,6 @@ return false; }); - setChapterMarkers(); - if ($isAndroidTvStore) { Mousetrap.bind('enter', () => { if (playerElement?.paused) { diff --git a/materialious/src/lib/components/Thumbnail.svelte b/materialious/src/lib/components/Thumbnail.svelte index db14d193..9640615a 100644 --- a/materialious/src/lib/components/Thumbnail.svelte +++ b/materialious/src/lib/components/Thumbnail.svelte @@ -5,7 +5,7 @@ import { onDestroy, onMount } from 'svelte'; import { _ } from '$lib/i18n'; import { get } from 'svelte/store'; - import { getDeArrow, getThumbnail, getVideoProgress } from '../api'; + import { getDeArrow, getThumbnail } from '../api'; import type { Notification, PlaylistPageVideo, Video, VideoBase } from '../api/model'; import { insecureRequestImageHandler, truncate } from '../misc'; import type { PlayerEvents } from '../player'; @@ -21,6 +21,7 @@ synciousStore } from '../store'; import { goto } from '$app/navigation'; + import { queueSyncious } from '$lib/api/apiExtended'; interface Props { video: VideoBase | Video | Notification | PlaylistPageVideo; @@ -109,7 +110,7 @@ if (get(synciousStore) && get(synciousInstanceStore) && get(authStore)) { try { - progress = (await getVideoProgress(video.videoId, { priority: 'low' }))[0].time.toString(); + progress = (await queueSyncious(video.videoId))?.time?.toString() ?? undefined; } catch {} } }); diff --git a/materialious/src/lib/watch.ts b/materialious/src/lib/watch.ts new file mode 100644 index 00000000..d0979319 --- /dev/null +++ b/materialious/src/lib/watch.ts @@ -0,0 +1,71 @@ +import { + amSubscribed, + getComments, + getDislikes, + getPersonalPlaylists, + getVideo, + postHistory +} from '$lib/api/index'; +import { loadEntirePlaylist } from '$lib/playlist'; +import { + authStore, + playerProxyVideosStore, + returnYTDislikesInstanceStore, + returnYtDislikesStore +} from '$lib/store'; +import { phaseDescription } from '$lib/timestamps'; +import { error } from '@sveltejs/kit'; +import { get } from 'svelte/store'; + +export async function getWatchDetails(videoId: string, url: URL) { + let video; + try { + video = await getVideo(videoId, get(playerProxyVideosStore), { priority: 'high' }); + } catch (errorMessage: any) { + error(500, errorMessage); + } + + let personalPlaylists; + if (get(authStore)) { + postHistory(video.videoId); + personalPlaylists = getPersonalPlaylists({ priority: 'low' }); + } else { + personalPlaylists = null; + } + + let comments; + try { + comments = video.liveNow + ? null + : getComments(videoId, { sort_by: 'top', source: 'youtube' }, { priority: 'low' }); + } catch { + comments = null; + } + + let returnYTDislikes; + const returnYTDislikesInstance = get(returnYTDislikesInstanceStore); + if (returnYTDislikesInstance && returnYTDislikesInstance !== '') { + try { + returnYTDislikes = get(returnYtDislikesStore) + ? getDislikes(videoId, { priority: 'low' }) + : null; + } catch {} + } + + const playlistId = url.searchParams.get('playlist'); + if (playlistId) { + await loadEntirePlaylist(playlistId); + } + + return { + video: video, + content: phaseDescription(video.videoId, video.descriptionHtml, video.fallbackPatch), + playlistId: playlistId, + streamed: { + personalPlaylists: personalPlaylists, + returnYTDislikes: returnYTDislikes, + comments: comments, + subscribed: amSubscribed(video.authorId) + } + }; +} diff --git a/materialious/src/routes/(app)/watch/[slug]/+page.ts b/materialious/src/routes/(app)/watch/[slug]/+page.ts index 8dede5ab..ddb95130 100644 --- a/materialious/src/routes/(app)/watch/[slug]/+page.ts +++ b/materialious/src/routes/(app)/watch/[slug]/+page.ts @@ -1,71 +1,5 @@ -import { - amSubscribed, - getComments, - getDislikes, - getPersonalPlaylists, - getVideo, - postHistory -} from '$lib/api/index'; -import { loadEntirePlaylist } from '$lib/playlist'; -import { - authStore, - playerProxyVideosStore, - returnYTDislikesInstanceStore, - returnYtDislikesStore -} from '$lib/store'; -import { phaseDescription } from '$lib/timestamps'; -import { error } from '@sveltejs/kit'; -import { get } from 'svelte/store'; +import { getWatchDetails } from '$lib/watch.js'; export async function load({ params, url }) { - let video; - try { - video = await getVideo(params.slug, get(playerProxyVideosStore), { priority: 'high' }); - } catch (errorMessage: any) { - error(500, errorMessage); - } - - let personalPlaylists; - if (get(authStore)) { - postHistory(video.videoId); - personalPlaylists = getPersonalPlaylists({ priority: 'low' }); - } else { - personalPlaylists = null; - } - - let comments; - try { - comments = video.liveNow - ? null - : getComments(params.slug, { sort_by: 'top', source: 'youtube' }, { priority: 'low' }); - } catch { - comments = null; - } - - let returnYTDislikes; - const returnYTDislikesInstance = get(returnYTDislikesInstanceStore); - if (returnYTDislikesInstance && returnYTDislikesInstance !== '') { - try { - returnYTDislikes = get(returnYtDislikesStore) - ? getDislikes(params.slug, { priority: 'low' }) - : null; - } catch {} - } - - const playlistId = url.searchParams.get('playlist'); - if (playlistId) { - await loadEntirePlaylist(playlistId); - } - - return { - video: video, - content: phaseDescription(video.videoId, video.descriptionHtml, video.fallbackPatch), - playlistId: playlistId, - streamed: { - personalPlaylists: personalPlaylists, - returnYTDislikes: returnYTDislikes, - comments: comments, - subscribed: amSubscribed(video.authorId) - } - }; + return getWatchDetails(params.slug, url); } diff --git a/materialious/src/routes/(no-layout)/tv/[slug]/+page.ts b/materialious/src/routes/(no-layout)/tv/[slug]/+page.ts index 8e058c6c..ddb95130 100644 --- a/materialious/src/routes/(no-layout)/tv/[slug]/+page.ts +++ b/materialious/src/routes/(no-layout)/tv/[slug]/+page.ts @@ -1,22 +1,5 @@ -import { getVideo } from '$lib/api/index'; -import { playerProxyVideosStore } from '$lib/store'; -import { error } from '@sveltejs/kit'; -import { get } from 'svelte/store'; +import { getWatchDetails } from '$lib/watch.js'; -export async function load({ params }) { - let video; - try { - video = await getVideo(params.slug, get(playerProxyVideosStore)); - } catch (errorMessage: any) { - error(500, errorMessage); - } - - return { - video: video, - content: { - description: '', - timestamps: [] - }, - playlistId: null - }; +export async function load({ params, url }) { + return getWatchDetails(params.slug, url); }