From f967ae48cbcf82423e1ddfacd91210a7c749b073 Mon Sep 17 00:00:00 2001 From: ward Date: Mon, 23 Jun 2025 11:42:52 +1200 Subject: [PATCH] Major playlist loading improvements --- materialious/electron/package-lock.json | 4 +- materialious/src/lib/components/Player.svelte | 65 ++++++++- .../src/lib/components/Thumbnail.svelte | 2 +- materialious/src/lib/playlist.ts | 53 +++++++ materialious/src/lib/store.ts | 13 +- .../routes/(app)/playlist/[slug]/+page.svelte | 16 +-- .../routes/(app)/watch/[slug]/+page.svelte | 130 +++--------------- .../src/routes/(app)/watch/[slug]/+page.ts | 19 ++- 8 files changed, 169 insertions(+), 133 deletions(-) create mode 100644 materialious/src/lib/playlist.ts diff --git a/materialious/electron/package-lock.json b/materialious/electron/package-lock.json index c81063ad..c24ee22d 100644 --- a/materialious/electron/package-lock.json +++ b/materialious/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "Materialious", - "version": "1.9.12", + "version": "1.9.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "Materialious", - "version": "1.9.12", + "version": "1.9.14", "license": "MIT", "dependencies": { "@capacitor-community/electron": "^5.0.0", diff --git a/materialious/src/lib/components/Player.svelte b/materialious/src/lib/components/Player.svelte index d8ce271c..b554f294 100644 --- a/materialious/src/lib/components/Player.svelte +++ b/materialious/src/lib/components/Player.svelte @@ -20,12 +20,13 @@ import { _ } from '$lib/i18n'; import { get } from 'svelte/store'; import { deleteVideoProgress, getVideoProgress, saveVideoProgress } from '../api'; - import type { VideoPlay } from '../api/model'; + import type { PlaylistPageVideo, VideoPlay } from '../api/model'; import { authStore, darkModeStore, instanceStore, playerAndroidLockOrientation, + playerAutoplayNextByDefaultStore, playerAutoPlayStore, playerDefaultLanguage, playerDefaultPlaybackSpeed, @@ -34,12 +35,14 @@ playerSavePlaybackPositionStore, playerStatisticsByDefault, playerYouTubeJsFallback, + playlistSettingsStore, sponsorBlockCategoriesStore, sponsorBlockDisplayToastStore, sponsorBlockStore, sponsorBlockUrlStore, synciousInstanceStore, synciousStore, + syncPartyConnectionsStore, themeColorStore } from '../store'; import { getDynamicTheme, setStatusBarColor } from '../theme'; @@ -48,6 +51,10 @@ import { playbackRates } from '$lib/const'; import { EndTimeElement } from '$lib/shaka-elements/endTime'; import androidTv from '$lib/android/plugins/androidTv'; + import { loadEntirePlaylist } from '$lib/playlist'; + import { goto } from '$app/navigation'; + import { unsafeRandomItem } from '$lib/misc'; + import type { PlayerEvents } from '$lib/player'; interface Props { data: { video: VideoPlay; content: PhasedDescription; playlistId: string | null }; @@ -78,6 +85,7 @@ const STORAGE_KEY_VOLUME = 'shaka-preferred-volume'; async function updateSeekBarTheme() { + if (!shakaUi) return; await tick(); shakaUi.configure({ seekBarColors: { @@ -589,6 +597,56 @@ }); } + playerElement.addEventListener('ended', async () => { + if (!data.playlistId) { + if ($playerAutoplayNextByDefaultStore) { + goto(`/watch/${data.video.recommendedVideos[0].videoId}`); + } + + return; + } + + const playlist = await loadEntirePlaylist(data.playlistId); + const playlistVideoIds = playlist.videos.map((value) => { + return value.videoId; + }); + + let goToVideo: PlaylistPageVideo | undefined; + + const shufflePlaylist = $playlistSettingsStore[data.playlistId]?.shuffle ?? false; + const loopPlaylist = $playlistSettingsStore[data.playlistId]?.loop ?? false; + + if (shufflePlaylist) { + goToVideo = unsafeRandomItem(playlist.videos); + } else { + const currentVideoIndex = playlistVideoIds.indexOf(data.video.videoId); + const newIndex = currentVideoIndex + 1; + if (currentVideoIndex !== -1 && newIndex < playlistVideoIds.length) { + goToVideo = playlist.videos[newIndex]; + } else if (loopPlaylist) { + // Loop playlist on end + goToVideo = playlist.videos[0]; + } + } + + if (typeof goToVideo !== 'undefined') { + if ($syncPartyConnectionsStore) { + $syncPartyConnectionsStore.forEach((conn) => { + if (typeof goToVideo === 'undefined') return; + + conn.send({ + events: [ + { type: 'change-video', videoId: goToVideo.videoId }, + { type: 'playlist', playlistId: data.playlistId } + ] + } as PlayerEvents); + }); + } + + goto(`/watch/${goToVideo.videoId}?playlist=${data.playlistId}`); + } + }); + try { await loadVideo(); } catch (error: unknown) { @@ -769,6 +827,11 @@ aspect-ratio: 16 / 9; } + video[poster] { + height: 100%; + width: 100%; + } + video { position: absolute; top: 50%; diff --git a/materialious/src/lib/components/Thumbnail.svelte b/materialious/src/lib/components/Thumbnail.svelte index 64ea8354..db14d193 100644 --- a/materialious/src/lib/components/Thumbnail.svelte +++ b/materialious/src/lib/components/Thumbnail.svelte @@ -152,7 +152,7 @@ placeholderHeight = innerWidth / 12; } } else { - placeholderHeight = 115; + placeholderHeight = 100; } } diff --git a/materialious/src/lib/playlist.ts b/materialious/src/lib/playlist.ts new file mode 100644 index 00000000..244c638e --- /dev/null +++ b/materialious/src/lib/playlist.ts @@ -0,0 +1,53 @@ +import { get } from 'svelte/store'; +import { getPlaylist } from './api'; +import type { PlaylistPage, PlaylistPageVideo } from './api/model'; +import { playlistCacheStore } from './store'; + +export async function loadEntirePlaylist( + playlistId: string +): Promise<{ videos: PlaylistPageVideo[]; info: PlaylistPage }> { + const cachedPlaylists = get(playlistCacheStore); + if (playlistId in cachedPlaylists) { + console.log('Using cache'); + return cachedPlaylists[playlistId]; + } + + let playlistVideos: PlaylistPageVideo[] = []; + let playlist: PlaylistPage | undefined = undefined; + + const ignoreVideos: string[] = []; + + for (let page = 1; page < Infinity; page++) { + const newPlaylist = await getPlaylist(playlistId, page); + if (page === 1) { + playlist = newPlaylist; + } + let newVideos = newPlaylist.videos; + if (newVideos.length === 0) { + break; + } + + newVideos = newVideos.filter((playlistVideo) => { + return playlistVideo.lengthSeconds > 0 && !ignoreVideos.includes(playlistVideo.videoId); + }); + + newVideos.forEach((playlistVideo) => { + ignoreVideos.push(playlistVideo.videoId); + }); + + playlistVideos = [...playlistVideos, ...newVideos].sort( + (a: PlaylistPageVideo, b: PlaylistPageVideo) => { + return a.index < b.index ? -1 : 1; + } + ); + } + + if (typeof playlist === 'undefined') { + throw new Error('Unable to fetch playlist'); + } + + const combined = { videos: playlistVideos, info: playlist }; + playlistCacheStore.set({ [playlistId]: combined }); + + return combined; +} diff --git a/materialious/src/lib/store.ts b/materialious/src/lib/store.ts index 1f21ef50..e1cbbc2e 100644 --- a/materialious/src/lib/store.ts +++ b/materialious/src/lib/store.ts @@ -4,7 +4,15 @@ import type { DataConnection } from 'peerjs'; import { persisted } from 'svelte-persisted-store'; import { writable, type Writable } from 'svelte/store'; import type { TitleCase } from './letterCasing'; -import type { Channel, HashTag, Playlist, PlaylistPageVideo, Video, VideoBase } from './api/model'; +import type { + Channel, + HashTag, + Playlist, + PlaylistPage, + PlaylistPageVideo, + Video, + VideoBase +} from './api/model'; import { ensureNoTrailingSlash } from './misc'; function platformDependentDefault(givenValue: any, defaultValue: any): any { @@ -130,5 +138,8 @@ export const searchCacheStore: Writable<{ [searchTypeAndQuery: string]: (Channel | Video | Playlist | HashTag)[]; }> = writable({}); export const feedLastItemId: Writable = writable(undefined); +export const playlistCacheStore: Writable<{ + [playlistId: string]: { videos: PlaylistPageVideo[]; info: PlaylistPage }; +}> = writable({}); export const isAndroidTvStore: Writable = writable(false); diff --git a/materialious/src/routes/(app)/playlist/[slug]/+page.svelte b/materialious/src/routes/(app)/playlist/[slug]/+page.svelte index 89e308be..1111452e 100644 --- a/materialious/src/routes/(app)/playlist/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/playlist/[slug]/+page.svelte @@ -9,6 +9,7 @@ import { Capacitor } from '@capacitor/core'; import { onMount } from 'svelte'; import { _ } from '$lib/i18n'; + import { loadEntirePlaylist } from '$lib/playlist.js'; let { data } = $props(); @@ -24,20 +25,7 @@ }); onMount(async () => { - for (let page = 1; page++; ) { - const newVideos = (await getPlaylist(data.playlist.playlistId, page)).videos; - if (newVideos.length === 0) { - break; - } - videos = [...(videos as PlaylistPageVideo[]), ...newVideos].sort( - (a: PlaylistPageVideo, b: PlaylistPageVideo) => { - return a.index < b.index ? -1 : 1; - } - ); - videos = videos.filter((playlistVideo) => { - return playlistVideo.lengthSeconds > 0; - }); - } + videos = (await loadEntirePlaylist(data.playlist.playlistId)).videos; }); } diff --git a/materialious/src/routes/(app)/watch/[slug]/+page.svelte b/materialious/src/routes/(app)/watch/[slug]/+page.svelte index 1266bac9..1b7e45ea 100644 --- a/materialious/src/routes/(app)/watch/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/watch/[slug]/+page.svelte @@ -1,15 +1,13 @@