Skip to content

Commit 6dfac7c

Browse files
committed
feat: paginate through playlist tracks
1 parent 2d51366 commit 6dfac7c

File tree

10 files changed

+217
-153
lines changed

10 files changed

+217
-153
lines changed

src/lib/index/sections/music/music-section.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script lang="ts">
22
import AppleMusicIcon from '$lib/icons/apple-music-icon.svelte';
33
import Section from '$lib/index/section.svelte';
4-
import type { CacheData } from '$lib/lcp/applemusic.server';
4+
import type { CacheData } from '$lib/lcp/applemusic';
55
import type { LcpResponse } from '$lib/lcp/lcp.server';
66
import Loading from '$lib/loading.svelte';
77
import { Error } from '@gleich/ui';

src/lib/index/sections/music/playlist.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
import type { AppleMusicPlaylistSummary } from '$lib/lcp/applemusic.server';
2+
import type { AppleMusicPlaylistSummary } from '$lib/lcp/applemusic';
33
import { Card, Image } from '@gleich/ui';
44
55
const { playlist }: { playlist: AppleMusicPlaylistSummary } = $props();

src/lib/index/sections/music/song.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script lang="ts">
22
import PauseIcon from '$lib/icons/pause-icon.svelte';
33
import PlayIcon from '$lib/icons/play-icon.svelte';
4-
import type { AppleMusicSong } from '$lib/lcp/applemusic.server';
4+
import type { AppleMusicSong } from '$lib/lcp/applemusic';
55
import { Card, Image, Scrolling } from '@gleich/ui';
66
import { currentAudio } from './playing';
77
Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
1-
import { LCP_TOKEN } from '$env/static/private';
21
import type { SvelteFetch } from './lcp.server';
2+
import type { Pagination } from './pagination';
33

44
export async function loadPlaylistFromLCP(
55
id: string,
6+
page: number,
67
fetch: SvelteFetch
7-
): Promise<AppleMusicPlaylist | null> {
8+
): Promise<AppleMusicPlaylistResponse | null> {
89
try {
9-
const res = await fetch(`https://lcp.mattglei.ch/applemusic/playlists/${id}`, {
10+
const res = await fetch(`http://localhost:8000/applemusic/playlists/${id}?page=${page}`, {
1011
method: 'GET',
11-
cache: 'no-store',
12-
headers: {
13-
Authorization: `Bearer ${LCP_TOKEN}`
14-
}
12+
cache: 'no-store'
1513
});
1614
return await res.json();
1715
} catch {
@@ -35,9 +33,16 @@ export interface AppleMusicSong {
3533
preview_audio_url: string | null;
3634
}
3735

36+
export interface AppleMusicPlaylistResponse {
37+
playlist: AppleMusicPlaylist;
38+
pagination: Pagination;
39+
}
40+
3841
export interface AppleMusicPlaylist {
3942
name: string;
4043
id: string;
44+
duration_in_millis: number;
45+
track_count: number;
4146
tracks: AppleMusicSong[];
4247
last_modified: Date;
4348
url: string;

src/lib/lcp/pagination.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface Pagination {
2+
current: number;
3+
total: number;
4+
next: number | null;
5+
}

src/routes/+page.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { CacheData } from '$lib/lcp/applemusic.server';
1+
import type { CacheData } from '$lib/lcp/applemusic';
22
import type { Repository } from '$lib/lcp/github';
33
import { Cache, loadFromLCP, type LcpResponse, type SvelteFetch } from '$lib/lcp/lcp.server';
44
import type { Game } from '$lib/lcp/steam';
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { loadPlaylistFromLCP, type AppleMusicPlaylist } from '$lib/lcp/applemusic.server';
1+
import { loadPlaylistFromLCP, type AppleMusicPlaylistResponse } from '$lib/lcp/applemusic';
22
import { type SvelteFetch } from '$lib/lcp/lcp.server';
33
import type { PageServerLoad } from './$types';
44

55
export interface PlaylistData {
6-
playlist: AppleMusicPlaylist | undefined;
6+
response: AppleMusicPlaylistResponse | undefined;
77
}
88

99
export const load: PageServerLoad = async ({
@@ -13,5 +13,5 @@ export const load: PageServerLoad = async ({
1313
params: Record<string, string>;
1414
fetch: SvelteFetch;
1515
}) => ({
16-
playlist: await loadPlaylistFromLCP(params.playlistID, fetch)
16+
response: await loadPlaylistFromLCP(params.id, 1, fetch)
1717
});
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
<script lang="ts">
2+
import AppleMusicIcon from '$lib/icons/apple-music-icon.svelte';
3+
import Song from '$lib/index/sections/music/song.svelte';
4+
import { renderDuration } from '$lib/time';
5+
import Since from '$lib/time/since.svelte';
6+
import ViewButton from '$lib/view-button.svelte';
7+
import { DynamicHead, Error } from '@gleich/ui';
8+
import SpotifyIcon from '$lib/icons/spotify-icon.svelte';
9+
import { loadPlaylistFromLCP } from '$lib/lcp/applemusic';
10+
import type { PlaylistData } from './+page.server';
11+
import { onMount } from 'svelte';
12+
13+
const { data }: { data: PlaylistData } = $props();
14+
15+
let tracks = $derived(data.response?.playlist?.tracks ?? []);
16+
let currentPage = $state(1);
17+
let loading = $state(false);
18+
let hasMore = $derived(
19+
data.response ? tracks.length < data.response.playlist.track_count : false
20+
);
21+
22+
onMount(() => {
23+
tracks = data.response?.playlist.tracks ?? [];
24+
currentPage = 1;
25+
loading = false;
26+
hasMore = data.response ? tracks.length < data.response?.playlist.track_count : false;
27+
});
28+
29+
async function loadNextPage() {
30+
if (!data || !data.response || loading || !hasMore) return;
31+
32+
loading = true;
33+
try {
34+
const nextPage = currentPage + 1;
35+
console.log('loading more from lcp');
36+
const next = await loadPlaylistFromLCP(data.response?.playlist.id, nextPage, fetch);
37+
const nextTracks = next?.playlist?.tracks ?? [];
38+
39+
if (nextTracks.length > 0) {
40+
tracks = [...tracks, ...nextTracks];
41+
currentPage = nextPage;
42+
}
43+
44+
if (!next?.pagination.next) {
45+
hasMore = false;
46+
return;
47+
}
48+
49+
hasMore = tracks.length < data.response.playlist.track_count;
50+
} catch (e) {
51+
console.error(e);
52+
} finally {
53+
loading = false;
54+
}
55+
}
56+
57+
function onScroll() {
58+
const threshold = 400;
59+
60+
const scrollTop = window.scrollY || document.documentElement.scrollTop;
61+
const viewportHeight = window.innerHeight;
62+
const docHeight = document.documentElement.scrollHeight;
63+
64+
const nearBottom = scrollTop + viewportHeight >= docHeight - threshold;
65+
66+
if (nearBottom) {
67+
void loadNextPage();
68+
}
69+
}
70+
</script>
71+
72+
<svelte:window on:scroll={onScroll} />
73+
74+
{#if data.response}
75+
<DynamicHead
76+
title={`${data.response.playlist.name} Playlist`}
77+
description={`${data.response.playlist.tracks.length} tracks`}
78+
opengraphImage={data.response.playlist?.tracks[0].album_art_url != null
79+
? { url: data.response.playlist?.tracks[0].album_art_url, height: '600', width: '600' }
80+
: undefined}
81+
/>
82+
{:else}
83+
<DynamicHead title="404 Not found" description="Playlist Not Found" />
84+
{/if}
85+
86+
{#if data.response}
87+
<div class="header">
88+
<div class="header-info">
89+
<h2>{data.response.playlist.name} Playlist</h2>
90+
<div class="stats">
91+
<p>Last updated <Since time={data.response.playlist.last_modified} /></p>
92+
<p>
93+
{data.response.playlist.track_count} songs - {renderDuration(
94+
data.response.playlist.duration_in_millis / 1000
95+
)}
96+
</p>
97+
</div>
98+
</div>
99+
<div class="view-on-buttons">
100+
<a
101+
class="view-on-button"
102+
href={`https://open.spotify.com/playlist/${data.response.playlist.spotify_id}`}
103+
target="_blank"
104+
>
105+
<ViewButton on="Spotify" icon={SpotifyIcon} iconPaddingBottom="1px" iconColor="#24db68" />
106+
</a>
107+
<a class="view-on-button" href={data.response.playlist.url} target="_blank">
108+
<ViewButton
109+
on="Apple Music"
110+
icon={AppleMusicIcon}
111+
iconPaddingBottom="0.5px"
112+
iconColor="#fb455d"
113+
/>
114+
</a>
115+
</div>
116+
</div>
117+
<div class="songs">
118+
{#each tracks as song (song)}
119+
<div class="song">
120+
<Song {song} />
121+
</div>
122+
{/each}
123+
</div>
124+
{:else}
125+
<Error msg="404: Playlist not found" />
126+
{/if}
127+
128+
<style>
129+
.header {
130+
display: flex;
131+
margin-bottom: 30px;
132+
flex-direction: column;
133+
}
134+
135+
.header-info {
136+
display: flex;
137+
justify-content: space-between;
138+
}
139+
140+
.stats {
141+
color: grey;
142+
display: flex;
143+
flex-direction: column;
144+
align-items: flex-end;
145+
}
146+
147+
.songs {
148+
display: flex;
149+
flex-wrap: wrap;
150+
gap: 10px;
151+
align-items: center;
152+
justify-content: center;
153+
margin-bottom: 20px;
154+
}
155+
156+
.song {
157+
width: 192px;
158+
}
159+
160+
.view-on-buttons {
161+
display: flex;
162+
gap: 10px;
163+
margin-top: 30px;
164+
margin-bottom: 10px;
165+
}
166+
167+
.view-on-button {
168+
flex: 1;
169+
text-decoration: inherit;
170+
}
171+
172+
@media (max-width: 550px) {
173+
.header-info {
174+
flex-direction: column;
175+
}
176+
177+
.song {
178+
width: calc(50% - 5px);
179+
}
180+
}
181+
182+
@media (max-width: 500px) {
183+
.stats {
184+
align-items: flex-start;
185+
}
186+
187+
.view-on-buttons {
188+
flex-direction: column;
189+
gap: 10px;
190+
}
191+
}
192+
</style>

0 commit comments

Comments
 (0)