list songs based on search
This commit is contained in:
parent
326d080e2c
commit
f444782ab2
@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + Svelte + TS</title>
|
<title>Discord music bot remote</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ArtistList from "./components/ArtistList.svelte";
|
import ArtistList from "./components/ArtistList.svelte";
|
||||||
|
import Search from './components/Search.svelte'; // Import the Search component
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<h1>Navidrome Frontend</h1>
|
<h1>Navidrome Frontend</h1>
|
||||||
|
<Search /> <!-- Add the Search component -->
|
||||||
<ArtistList />
|
<ArtistList />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
180
subsonic-ui-svelte/src/components/Search.svelte
Normal file
180
subsonic-ui-svelte/src/components/Search.svelte
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { fetchFromAPI } from "../lib/api"; // Assuming you have the `fetchFromAPI` function available
|
||||||
|
import { debounce } from "../lib/api";
|
||||||
|
|
||||||
|
let query = ""; // Search query input by user
|
||||||
|
let searchResults: any[] = []; // Store search results
|
||||||
|
let loading = false; // Loading state
|
||||||
|
let error: string = ""; // Error message
|
||||||
|
let coverArtUrls: Record<string, string> = {}; // Map to store cover art URLs for each album
|
||||||
|
|
||||||
|
// Function to handle search
|
||||||
|
const searchTracks = async () => {
|
||||||
|
if (!query.trim()) return; // Don't search if the query is empty
|
||||||
|
|
||||||
|
loading = true;
|
||||||
|
error = "";
|
||||||
|
try {
|
||||||
|
const params = { query }; // Search query parameter
|
||||||
|
const response = await fetchFromAPI("search2", params); // Use 'search2' endpoint to search for tracks
|
||||||
|
const songs = response["subsonic-response"].searchResult2.song; // Assuming 'searchResult2.song' contains the song list
|
||||||
|
|
||||||
|
// Map the search results to the component
|
||||||
|
searchResults = songs;
|
||||||
|
|
||||||
|
// For each song, fetch the cover art if not already loaded
|
||||||
|
for (const song of songs) {
|
||||||
|
if (!coverArtUrls[song.albumId]) {
|
||||||
|
// Fetch cover art for the album (we'll assume the albumId exists)
|
||||||
|
const coverArtUrl = await fetchCoverArt(song.albumId);
|
||||||
|
coverArtUrls[song.albumId] = coverArtUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error = "Error fetching search results";
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch the cover art image URL for the given album ID
|
||||||
|
const fetchCoverArt = async (albumId: string): Promise<string> => {
|
||||||
|
const params = { id: albumId, size: "50" }; // Size parameter for resizing
|
||||||
|
const response = await fetchFromAPI("getCoverArt", params);
|
||||||
|
const coverArtUrl = URL.createObjectURL(response);
|
||||||
|
return coverArtUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDuration = (seconds: number): string => {
|
||||||
|
const minutes = Math.floor(seconds / 60); // Get the full minutes
|
||||||
|
const remainingSeconds = seconds % 60; // Get the remaining seconds
|
||||||
|
return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`; // Ensure seconds are always 2 digits
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInputChange = () => {
|
||||||
|
debounce(searchTracks, 500); // 500ms debounce delay
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="search-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={query}
|
||||||
|
placeholder="Search for a song"
|
||||||
|
on:input={onInputChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<div class="loading">Loading search results...</div>
|
||||||
|
{:else if error}
|
||||||
|
<div class="error">{error}</div>
|
||||||
|
{:else if searchResults.length > 0}
|
||||||
|
<ul class="search-results">
|
||||||
|
{#each searchResults as result, index}
|
||||||
|
<li class="search-result">
|
||||||
|
<!-- Display the search result number -->
|
||||||
|
<span class="result-number">{index + 1}.</span>
|
||||||
|
<!-- Display the album cover art -->
|
||||||
|
<img
|
||||||
|
src={coverArtUrls[result.albumId] ||
|
||||||
|
"/default-cover.jpg"}
|
||||||
|
alt={result.album}
|
||||||
|
class="album-cover"
|
||||||
|
/>
|
||||||
|
<div class="track-details">
|
||||||
|
<h4>{result.title}</h4>
|
||||||
|
<!-- Track name -->
|
||||||
|
<p>{result.artist}</p>
|
||||||
|
<!-- Artist name -->
|
||||||
|
</div>
|
||||||
|
<!-- Album and duration -->
|
||||||
|
<div class="album-duration">
|
||||||
|
<span>{formatDuration(result.duration)}</span>
|
||||||
|
<span>{result.album}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{:else}
|
||||||
|
<div class="loading">No results found</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.search-container {
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px; /* Space between elements */
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #ddd; /* Optional: add a separator */
|
||||||
|
position: relative; /* For precise positioning of the result number */
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-number {
|
||||||
|
font-weight: normal;
|
||||||
|
width: 40px; /* Fixed width for alignment */
|
||||||
|
text-align: right;
|
||||||
|
align-self: flex-end; /* Align with the bottom of the album image */
|
||||||
|
margin-bottom: 4px; /* Optional: fine-tune spacing */
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-cover {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px; /* Optional: rounded corners */
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-details {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-left: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-details h4 {
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.track-details p {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #555; /* Optional: gray color for artist name */
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-duration {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
text-align: right;
|
||||||
|
gap: 4px; /* Space between album name and duration */
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-duration span {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -3,27 +3,6 @@ const API_VERSION = "1.16.1"; // Replace with the Subsonic API version you're us
|
|||||||
const API_PASSWORD = "xWPciqjr5VSVDvVS0sAH9L8gKEQm42"; // Replace with your actual token
|
const API_PASSWORD = "xWPciqjr5VSVDvVS0sAH9L8gKEQm42"; // Replace with your actual token
|
||||||
const API_CLIENT = "SvelteApp";
|
const API_CLIENT = "SvelteApp";
|
||||||
|
|
||||||
// export async function fetchFromAPI(endpoint: string, params: Record<string, string>) {
|
|
||||||
// const url = new URL(`${API_URL}/${endpoint}`);
|
|
||||||
// url.search = new URLSearchParams({
|
|
||||||
// ...params,
|
|
||||||
// v: API_VERSION,
|
|
||||||
// c: API_CLIENT,
|
|
||||||
// f: "json", // Always request JSON format
|
|
||||||
// u: "drome", // Replace with your username
|
|
||||||
// p: API_PASSWORD
|
|
||||||
// //t: API_TOKEN, // Your token
|
|
||||||
// //s: "salt", // Optional if you're using salted tokens
|
|
||||||
// }).toString();
|
|
||||||
|
|
||||||
|
|
||||||
// const response = await fetch(url.toString());
|
|
||||||
// if (!response.ok) {
|
|
||||||
// throw new Error(`API request failed: ${response.statusText}`);
|
|
||||||
// }
|
|
||||||
// return response.json();
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
export async function fetchFromAPI(endpoint: string, params: Record<string, string>) {
|
export async function fetchFromAPI(endpoint: string, params: Record<string, string>) {
|
||||||
const url = new URL(`${API_URL}/${endpoint}`);
|
const url = new URL(`${API_URL}/${endpoint}`);
|
||||||
@ -65,3 +44,12 @@ export async function fetchFromAPI(endpoint: string, params: Record<string, stri
|
|||||||
// If it's a request for cover art, return the raw image data as a Blob
|
// If it's a request for cover art, return the raw image data as a Blob
|
||||||
return response.blob();
|
return response.blob();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let debounceTimeout: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
|
export const debounce = (callback: () => void, delay: number) => {
|
||||||
|
if (debounceTimeout) {
|
||||||
|
clearTimeout(debounceTimeout); // Clear the previous timeout
|
||||||
|
}
|
||||||
|
debounceTimeout = setTimeout(callback, delay);
|
||||||
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user