diff --git a/src/components/vue/SnapshotCalendar.vue b/src/components/vue/SnapshotCalendar.vue
index 8dadc35..2c76288 100644
--- a/src/components/vue/SnapshotCalendar.vue
+++ b/src/components/vue/SnapshotCalendar.vue
@@ -1,63 +1,111 @@
+
+
+
+ Failed to load playlist registry
+
+
+
+
+
{
no-today
prevent-min-max-navigation
inline
- @update-month-year="updateSearchParams"
+ @update-month-year="updateQueryAndReloadSnapshots"
>
+
+
+ {{ day }}
+
+
+ {{ day }}
+
+
@@ -109,6 +169,10 @@ onBeforeMount(() => {
@apply md:mx-2 md:my-1.5 mx-1.5 my-1 p-0 hover:bg-transparent hover:text-inherit;
}
+:deep(div.dp__cell_inner > a) {
+ @apply hover:opacity-75 hover:text-primary-content focus:text-primary-content active:text-primary-content rounded-full;
+}
+
:deep(div.dp__cell_disabled) {
@apply text-base-content opacity-30;
}
diff --git a/src/models/snapshot-meta.ts b/src/models/snapshot-meta.ts
new file mode 100644
index 0000000..7b23f13
--- /dev/null
+++ b/src/models/snapshot-meta.ts
@@ -0,0 +1,6 @@
+export interface SnapshotMeta {
+ snapshotId: string;
+ commitSha: string;
+ dateCaptured: string;
+ numFollowers: number;
+}
diff --git a/src/pages/playlists/[playlistId]/snapshots.astro b/src/pages/playlists/[playlistId]/snapshots.astro
index 51a2776..9c1b176 100644
--- a/src/pages/playlists/[playlistId]/snapshots.astro
+++ b/src/pages/playlists/[playlistId]/snapshots.astro
@@ -4,6 +4,7 @@ import SnapshotCalendar from '@/components/vue/SnapshotCalendar.vue';
import { getPlaylistLayoutProps } from '@/utils/getPlaylistLayoutProps';
+const playlistId = Astro.params.playlistId as string;
const layoutProps = await getPlaylistLayoutProps(Astro);
---
@@ -11,5 +12,5 @@ const layoutProps = await getPlaylistLayoutProps(Astro);
Click on a highlighted date to show a snapshot captured that day.
-
+
diff --git a/src/pages/playlists/[playlistId]/snapshots.json.ts b/src/pages/playlists/[playlistId]/snapshots.json.ts
new file mode 100644
index 0000000..db063b0
--- /dev/null
+++ b/src/pages/playlists/[playlistId]/snapshots.json.ts
@@ -0,0 +1,78 @@
+import type { APIRoute } from 'astro';
+import type { PlaylistSnapshot } from '@/models/playlist-snapshot';
+
+import { Octokit } from '@octokit/rest';
+import { queryParamsToDate } from '@/utils/queryParamsToDate';
+
+export const get: APIRoute = async ({ request, params }) => {
+ try {
+ const playlistId = params.playlistId;
+ const queryParams = new URLSearchParams(new URL(request.url).search);
+
+ const sinceDate = queryParamsToDate(queryParams);
+ const untilDate = new Date(
+ sinceDate.getFullYear(),
+ sinceDate.getMonth() + 1
+ );
+
+ const octokit = new Octokit();
+ const { data: commits } = await octokit.rest.repos.listCommits({
+ owner: 'mackorone',
+ repo: 'spotify-playlist-archive',
+ path: `playlists/pretty/${playlistId}.json`,
+ since: sinceDate.toISOString(),
+ until: untilDate.toISOString()
+ });
+
+ const possiblyDuplicateSnapshots = await Promise.all(
+ commits.map(async ({ sha, commit }) => {
+ const githubResponse = await fetch(
+ `https://raw.githubusercontent.com/mackorone/spotify-playlist-archive/${sha}/playlists/pretty/${playlistId}.json`
+ );
+
+ if (!githubResponse.ok) {
+ throw new Error(`GitHub ${githubResponse.status}`);
+ }
+
+ const { snapshot_id, num_followers } =
+ (await githubResponse.json()) as PlaylistSnapshot;
+
+ return {
+ snapshotId: snapshot_id,
+ commitSha: sha,
+ dateCaptured: commit.author?.date,
+ numFollowers: num_followers
+ };
+ })
+ );
+
+ const body = JSON.stringify(
+ queryParams.get('allowDuplicates') === 'yes'
+ ? possiblyDuplicateSnapshots
+ : // Since commits are sorted by the latest date_captured first,
+ // the following code will preserve the last item with a duplicate snapshot_id value.
+ // Therefore, we'll be left with entries containing the earliest date_captured.
+ [
+ ...new Map(
+ possiblyDuplicateSnapshots.map((snapshot) => [
+ snapshot.snapshotId,
+ snapshot
+ ])
+ ).values()
+ ]
+ );
+
+ return {
+ headers: {
+ 'Cache-Control': 'max-age=86400'
+ },
+ body
+ };
+ } catch (error) {
+ console.error(error);
+ return new Response(null, {
+ status: 500,
+ statusText: 'Failed to load snapshots'
+ });
+ }
+};
diff --git a/src/utils/queryParamsToDate.ts b/src/utils/queryParamsToDate.ts
new file mode 100644
index 0000000..704afe4
--- /dev/null
+++ b/src/utils/queryParamsToDate.ts
@@ -0,0 +1,27 @@
+export const queryParamsToDate = (queryParams: URLSearchParams) => {
+ const today = new Date();
+
+ const year = queryParams.has('year')
+ ? Math.max(
+ Math.min(
+ Number(queryParams.get('year')) || today.getFullYear(),
+ today.getFullYear()
+ ),
+ 2021
+ )
+ : today.getFullYear();
+
+ const normalisedMonthParam = Math.min(
+ // Query param months aren't zero-based, but JS Date months are
+ Math.max((Number(queryParams.get('month')) || 1) - 1, 0),
+ 11
+ );
+
+ const month = queryParams.has('month')
+ ? year === today.getFullYear()
+ ? Math.min(normalisedMonthParam, today.getMonth())
+ : normalisedMonthParam
+ : today.getMonth();
+
+ return new Date(year, month);
+};