Major playlist loading improvements
This commit is contained in:
Generated
+2
-2
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user