Major playlist loading improvements

This commit is contained in:
ward
2025-06-23 11:42:52 +12:00
parent 7e8fbd9afc
commit f967ae48cb
8 changed files with 169 additions and 133 deletions
+2 -2
View File
@@ -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",
+64 -1
View File
@@ -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%;
@@ -152,7 +152,7 @@
placeholderHeight = innerWidth / 12;
}
} else {
placeholderHeight = 115;
placeholderHeight = 100;
}
}
</script>
+53
View File
@@ -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;
}
+12 -1
View File
@@ -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<string | undefined> = writable(undefined);
export const playlistCacheStore: Writable<{
[playlistId: string]: { videos: PlaylistPageVideo[]; info: PlaylistPage };
}> = writable({});
export const isAndroidTvStore: Writable<boolean> = writable(false);
@@ -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;
});
}
</script>
@@ -1,15 +1,13 @@
<script lang="ts">
import { goto } from '$app/navigation';
import {
addPlaylistVideo,
deleteUnsubscribe,
getComments,
getPersonalPlaylists,
getPlaylist,
postSubscribe,
removePlaylistVideo
} from '$lib/api/index';
import type { Comments, PlaylistPage, PlaylistPageVideo } from '$lib/api/model';
import type { Comments, PlaylistPage } from '$lib/api/model';
import Comment from '$lib/components/Comment.svelte';
import Player from '$lib/components/Player.svelte';
import ShareVideo from '$lib/components/ShareVideo.svelte';
@@ -17,7 +15,7 @@
import Transcript from '$lib/components/Transcript.svelte';
import { getBestThumbnail, proxyGoogleImage } from '$lib/images';
import { letterCase } from '$lib/letterCasing';
import { truncate, unsafeRandomItem } from '$lib/misc';
import { truncate } from '$lib/misc';
import { cleanNumber, humanizeSeconds, numberWithCommas } from '$lib/numbers';
import type { PlayerEvents } from '$lib/player.js';
import {
@@ -26,8 +24,8 @@
interfaceAutoExpandComments,
interfaceAutoExpandDesc,
interfaceLowBandwidthMode,
playerAutoplayNextByDefaultStore,
playerTheatreModeByDefaultStore,
playlistCacheStore,
playlistSettingsStore,
syncPartyConnectionsStore,
syncPartyPeerStore
@@ -38,6 +36,7 @@
import { onDestroy, onMount, tick } from 'svelte';
import { _ } from '$lib/i18n';
import { get } from 'svelte/store';
import { loadEntirePlaylist } from '$lib/playlist.js';
let { data = $bindable() } = $props();
@@ -56,9 +55,6 @@
let personalPlaylists: PlaylistPage[] | null = $state(null);
data.streamed.personalPlaylists?.then((streamPlaylists) => (personalPlaylists = streamPlaylists));
let playlistVideos: PlaylistPageVideo[] = $state([]);
let playlist: PlaylistPage | null = $state(null);
let loopPlaylist: boolean = $state(false);
let shufflePlaylist: boolean = $state(false);
@@ -72,8 +68,6 @@
let playerCurrentTime: number = $state(0);
let currentChapterStartTime: number = $state(0);
function expandSummery(id: string) {
const element = document.getElementById(id);
if (element) {
@@ -135,7 +129,7 @@
event.playlistId !== data.playlistId
) {
data.playlistId = event.playlistId;
await loadPlaylist(event.playlistId);
await loadEntirePlaylist(event.playlistId);
goToCurrentPlaylistItem();
}
});
@@ -227,6 +221,10 @@
});
onMount(async () => {
if (data.playlistId) {
await goToCurrentPlaylistItem();
}
if ($interfaceAutoExpandDesc) {
expandSummery('description');
}
@@ -245,70 +243,8 @@
playerElement.addEventListener('timeupdate', () => {
if (!playerElement) return;
playerCurrentTime = playerElement.currentTime;
if (data.content.timestamps) {
for (const timestamp of data.content.timestamps) {
if (timestamp.time >= playerCurrentTime && timestamp.endTime <= playerCurrentTime) {
currentChapterStartTime = timestamp.time;
break;
}
}
}
});
playerElement.addEventListener('ended', async () => {
if (playlistVideos.length === 0) {
if ($playerAutoplayNextByDefaultStore) {
goto(`/watch/${data.video.recommendedVideos[0].videoId}`);
}
return;
}
await goToCurrentPlaylistItem();
const playlistVideoIds = playlistVideos.map((value) => {
return value.videoId;
});
let goToVideo: PlaylistPageVideo | undefined;
if (shufflePlaylist) {
goToVideo = unsafeRandomItem(playlistVideos);
} else {
const currentVideoIndex = playlistVideoIds.indexOf(data.video.videoId);
const newIndex = currentVideoIndex + 1;
if (currentVideoIndex !== -1 && newIndex < playlistVideoIds.length) {
goToVideo = playlistVideos[newIndex];
} else if (loopPlaylist) {
// Loop playlist on end
goToVideo = playlistVideos[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}`);
}
});
}
if (!data.playlistId) return;
await loadPlaylist(data.playlistId);
await goToCurrentPlaylistItem();
});
onDestroy(() => {
@@ -320,28 +256,6 @@
}
});
async function loadPlaylist(playlistId: string) {
for (let page = 1; page < Infinity; page++) {
const newPlaylist = await getPlaylist(playlistId, page);
if (page === 1) {
playlist = newPlaylist;
}
const newVideos = newPlaylist.videos;
if (newVideos.length === 0) {
break;
}
playlistVideos = [...playlistVideos, ...newVideos].sort(
(a: PlaylistPageVideo, b: PlaylistPageVideo) => {
return a.index < b.index ? -1 : 1;
}
);
playlistVideos = playlistVideos.filter((playlistVideo) => {
return playlistVideo.lengthSeconds > 0;
});
}
}
async function goToCurrentPlaylistItem() {
await tick();
const playlistCurrentVideo = document.getElementById(data.video.videoId);
@@ -682,28 +596,30 @@
{#if showTranscript && playerElement}
<Transcript video={data.video} bind:playerElement />
{/if}
{#if playlist}
{#if data.playlistId && data.playlistId in $playlistCacheStore}
<article
style="height: 85vh; position: relative;"
style="height: 85vh; position: relative;scrollbar-width: none;"
id="playlist"
class="scroll no-padding surface-container-high"
>
<article class="no-elevate" style="position: sticky; top: 0; z-index: 3;">
<h6>{playlist.title}</h6>
<h6>{$playlistCacheStore[data.playlistId].info.title}</h6>
<p>
{cleanNumber(playlist.viewCount)}
{$_('views')}{playlist.videoCount}
{cleanNumber($playlistCacheStore[data.playlistId].info.viewCount)}
{$_('views')}{$playlistCacheStore[data.playlistId].info.videoCount}
{$_('videos')}
</p>
<p><a href={`/channel/${playlist.authorId}`}>{playlist.author}</a></p>
<p>
<a href={`/channel/${$playlistCacheStore[data.playlistId].info.authorId}`}
>{$playlistCacheStore[data.playlistId].info.author}</a
>
</p>
<nav>
<button
onclick={() => {
if (!playlist) return;
loopPlaylist = !loopPlaylist;
playlistSettingsStore.set({
[playlist.playlistId]: { loop: loopPlaylist, shuffle: shufflePlaylist }
[data.playlistId as string]: { loop: loopPlaylist, shuffle: shufflePlaylist }
});
}}
class="circle"
@@ -716,11 +632,9 @@
</button>
<button
onclick={() => {
if (!playlist) return;
shufflePlaylist = !shufflePlaylist;
playlistSettingsStore.set({
[playlist.playlistId]: { loop: loopPlaylist, shuffle: shufflePlaylist }
[data.playlistId as string]: { loop: loopPlaylist, shuffle: shufflePlaylist }
});
}}
class="circle"
@@ -739,7 +653,7 @@
<div class="space"></div>
{#each playlistVideos as playlistVideo}
{#each $playlistCacheStore[data.playlistId].videos as playlistVideo}
<article
class="no-padding primary-border"
style="margin: .7em;"
@@ -6,6 +6,7 @@ import {
getVideo,
postHistory
} from '$lib/api/index';
import { loadEntirePlaylist } from '$lib/playlist';
import {
authStore,
playerProxyVideosStore,
@@ -19,7 +20,7 @@ import { get } from 'svelte/store';
export async function load({ params, url }) {
let video;
try {
video = await getVideo(params.slug, get(playerProxyVideosStore), { priority: "high" });
video = await getVideo(params.slug, get(playerProxyVideosStore), { priority: 'high' });
} catch (errorMessage: any) {
error(500, errorMessage);
}
@@ -36,7 +37,7 @@ export async function load({ params, url }) {
try {
comments = video.liveNow
? null
: getComments(params.slug, { sort_by: 'top', source: 'youtube' }, { priority: "low" });
: getComments(params.slug, { sort_by: 'top', source: 'youtube' }, { priority: 'low' });
} catch {
comments = null;
}
@@ -45,20 +46,26 @@ export async function load({ params, url }) {
const returnYTDislikesInstance = get(returnYTDislikesInstanceStore);
if (returnYTDislikesInstance && returnYTDislikesInstance !== '') {
try {
returnYTDislikes = get(returnYtDislikesStore) ? getDislikes(params.slug, { priority: "low" }) : null;
} catch { }
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: url.searchParams.get('playlist'),
playlistId: playlistId,
streamed: {
personalPlaylists: personalPlaylists,
returnYTDislikes: returnYTDislikes,
comments: comments,
subscribed: amSubscribed(video.authorId),
subscribed: amSubscribed(video.authorId)
}
};
}