mirror of
https://github.com/maciejpedzich/spotifyplaylistarchive.com.git
synced 2024-09-19 18:16:19 +02:00
Implement playlist search bar
This commit is contained in:
parent
8ce2d9a509
commit
7a79bf93dc
14
models/playlist.ts
Normal file
14
models/playlist.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { User } from './user';
|
||||
import { Track } from './track';
|
||||
|
||||
export interface Playlist {
|
||||
id?: string;
|
||||
description: string;
|
||||
num_followers: number;
|
||||
original_name: string;
|
||||
owner: User;
|
||||
snapshot_id: string;
|
||||
tracks: Track[];
|
||||
unique_name: string;
|
||||
url: string;
|
||||
}
|
16
models/track.ts
Normal file
16
models/track.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { User } from './user';
|
||||
|
||||
export interface Track {
|
||||
added_at: string;
|
||||
album: {
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
artists: User[];
|
||||
duration_ms: number;
|
||||
name: string;
|
||||
url: string;
|
||||
date_added: string;
|
||||
date_added_asterisk: boolean;
|
||||
date_removed: string | null;
|
||||
}
|
4
models/user.ts
Normal file
4
models/user.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface User {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
141
pages/index.vue
Normal file
141
pages/index.vue
Normal file
@ -0,0 +1,141 @@
|
||||
<script setup lang="ts">
|
||||
import AutoComplete from 'primevue/autocomplete';
|
||||
import Message from 'primevue/message';
|
||||
import Button from 'primevue/button';
|
||||
import Skeleton from 'primevue/skeleton';
|
||||
|
||||
import { Searcher } from 'fast-fuzzy';
|
||||
|
||||
import { Playlist } from '@/models/playlist';
|
||||
import { getPlaylistIdFromUrl } from '@/utils/getPlaylistIdFromUrl';
|
||||
|
||||
interface SearchResult {
|
||||
id: string;
|
||||
original_name: string;
|
||||
display_name: string;
|
||||
}
|
||||
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
|
||||
const searchTerm = ref('');
|
||||
const searchResults = ref<SearchResult[]>([]);
|
||||
|
||||
const {
|
||||
pending: loadingPlaylistRegistry,
|
||||
data: playlistRegistry,
|
||||
error: playlistRegistryLoadError,
|
||||
refresh: reloadPlaylistRegistry
|
||||
} = useAsyncData<SearchResult[]>(
|
||||
'playlistRegistry',
|
||||
async () => {
|
||||
const registryFileContent = await $fetch<Record<string, Playlist>>(
|
||||
`${runtimeConfig.public.githubRawBaseUrl}/main/playlists/metadata.json`,
|
||||
{ parseResponse: JSON.parse }
|
||||
);
|
||||
|
||||
return Object.entries(registryFileContent).map(
|
||||
([id, { original_name, unique_name }]) => ({
|
||||
id,
|
||||
original_name,
|
||||
display_name:
|
||||
original_name === unique_name
|
||||
? original_name
|
||||
: `${unique_name} (${original_name})`
|
||||
})
|
||||
);
|
||||
},
|
||||
{ default: () => [] }
|
||||
);
|
||||
|
||||
const fuzzySearcher = new Searcher(playlistRegistry.value, {
|
||||
keySelector: (obj) => obj.original_name
|
||||
});
|
||||
|
||||
const performSearch = async () => {
|
||||
// Assume search term is a valid playlist URL and obtain a playlist ID
|
||||
const playlistId = getPlaylistIdFromUrl(searchTerm.value);
|
||||
|
||||
if (playlistId) {
|
||||
// Search term is a valid playlist URL
|
||||
// Find an object with an id equal to obtained playlistId
|
||||
const playlistUrlSearchResults = playlistRegistry.value.filter(
|
||||
(p) => p.id === playlistId
|
||||
);
|
||||
|
||||
searchResults.value = playlistUrlSearchResults;
|
||||
} else {
|
||||
// Search term is not a valid playlist URL
|
||||
// Perform a fuzzy search against playlists' original names
|
||||
// Limit results to 10 best matches
|
||||
const fuzzySearchResults = fuzzySearcher
|
||||
.search(searchTerm.value)
|
||||
.slice(0, 10);
|
||||
|
||||
searchResults.value = fuzzySearchResults;
|
||||
}
|
||||
};
|
||||
|
||||
const goToSnapshotCalendar = async ({
|
||||
value: playlist
|
||||
}: {
|
||||
value: SearchResult;
|
||||
}) => {
|
||||
await navigateTo(`/playlists/${playlist.id}`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-full h-full flex flex-column gap-2 justify-content-center align-items-center"
|
||||
>
|
||||
<h1 class="m-0 text-6xl">Spotify Playlist Archive</h1>
|
||||
<div>
|
||||
<p class="mt-0 mb-4 text-2xl">
|
||||
Browse past versions of thousands of playlists saved over time
|
||||
</p>
|
||||
<Skeleton v-if="loadingPlaylistRegistry" class="w-full h-3rem" />
|
||||
<Message
|
||||
v-else-if="playlistRegistryLoadError"
|
||||
class="text-0"
|
||||
severity="error"
|
||||
:closable="false"
|
||||
>
|
||||
<div class="w-full flex align-items-center">
|
||||
<span class="flex-1">Failed to load playlist search bar</span>
|
||||
<Button
|
||||
size="small"
|
||||
severity="danger"
|
||||
label="Try again"
|
||||
@click="reloadPlaylistRegistry()"
|
||||
/>
|
||||
</div>
|
||||
</Message>
|
||||
<AutoComplete
|
||||
v-else
|
||||
class="w-full"
|
||||
aria-label="Playlist Search Bar"
|
||||
:suggestions="searchResults"
|
||||
:min-length="3"
|
||||
placeholder="Start typing a playlist's title or paste its URL"
|
||||
field="display_name"
|
||||
@complete="performSearch"
|
||||
@item-select="goToSnapshotCalendar"
|
||||
v-model="searchTerm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-autocomplete-input) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.p-message-text) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.p-autocomplete-input::placeholder) {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
17
utils/getPlaylistIdFromUrl.ts
Normal file
17
utils/getPlaylistIdFromUrl.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export const getPlaylistIdFromUrl = (url: string) => {
|
||||
try {
|
||||
const urlObject = new URL(url);
|
||||
const [collectionName, playlistId] = urlObject.pathname
|
||||
.split('/')
|
||||
.filter(Boolean);
|
||||
|
||||
const isValidPlaylistUrl =
|
||||
urlObject.hostname === 'open.spotify.com' &&
|
||||
collectionName === 'playlist' &&
|
||||
playlistId;
|
||||
|
||||
return isValidPlaylistUrl ? playlistId : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue
Block a user