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 @@ @@ -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); +};