mirror of
https://github.com/wahyd4/you-music.git
synced 2026-08-08 20:59:47 +10:00
193 lines
6.9 KiB
TypeScript
193 lines
6.9 KiB
TypeScript
import { useState } from 'react'
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import { searchApi, downloadApi } from '@/api/client'
|
|
import { Music } from '@/types'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Download, Search as SearchIcon, Play } from 'lucide-react'
|
|
import { toast } from 'sonner'
|
|
|
|
interface SearchPageProps {
|
|
onPlayMusic: (music: Music) => void
|
|
}
|
|
|
|
export default function SearchPage({ onPlayMusic }: SearchPageProps) {
|
|
const [query, setQuery] = useState('')
|
|
const [searchQuery, setSearchQuery] = useState('')
|
|
const queryClient = useQueryClient()
|
|
|
|
const { data: results, isLoading } = useQuery({
|
|
queryKey: ['search', searchQuery],
|
|
queryFn: async () => {
|
|
if (!searchQuery) return { youtube: [], bilibili: [] }
|
|
const response = await searchApi.search(searchQuery)
|
|
return response.data
|
|
},
|
|
enabled: !!searchQuery,
|
|
})
|
|
|
|
const downloadMutation = useMutation({
|
|
mutationFn: ({ url, title }: { url: string; title: string; thumbnail?: string; artist?: string }) =>
|
|
downloadApi.downloadMusic({ url, title }),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['download-status'] })
|
|
toast.success('Download started! Check Download Center for progress.')
|
|
},
|
|
onError: () => {
|
|
toast.error('Download failed')
|
|
},
|
|
})
|
|
|
|
const handleSearch = (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
setSearchQuery(query)
|
|
}
|
|
|
|
const handleDownload = (url: string, title: string, thumbnail?: string, artist?: string) => {
|
|
downloadMutation.mutate({ url, title, thumbnail, artist })
|
|
}
|
|
|
|
const handlePlayAndDownload = async (url: string, title: string, thumbnail?: string, artist?: string) => {
|
|
// Start download
|
|
downloadMutation.mutate({ url, title, thumbnail, artist })
|
|
|
|
// Create temporary music object for streaming playback
|
|
const streamUrl = `/api/stream?url=${encodeURIComponent(url)}`
|
|
const tempMusic: Music = {
|
|
id: 0,
|
|
title,
|
|
artist: artist || null,
|
|
album: null,
|
|
duration: null,
|
|
file_path: streamUrl,
|
|
file_size: null,
|
|
source_url: url,
|
|
source_type: url.includes('youtube') ? 'youtube' : 'bilibili',
|
|
thumbnail: thumbnail || null,
|
|
lyrics: null,
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString(),
|
|
file_format: null,
|
|
file_location: null,
|
|
file_exists: true,
|
|
last_scanned_at: null,
|
|
}
|
|
|
|
onPlayMusic(tempMusic)
|
|
toast.success('Playing while downloading...')
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-screen-xl mx-auto p-4">
|
|
<form onSubmit={handleSearch} className="mb-6">
|
|
<div className="flex gap-2">
|
|
<Input
|
|
type="text"
|
|
placeholder="Search for music..."
|
|
value={query}
|
|
onChange={(e) => setQuery(e.target.value)}
|
|
className="flex-1"
|
|
/>
|
|
<Button type="submit" disabled={isLoading}>
|
|
<SearchIcon className="h-5 w-5" />
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
|
|
{isLoading && <div>Searching...</div>}
|
|
|
|
{results && (
|
|
<div className="space-y-6">
|
|
{results.youtube?.length > 0 && (
|
|
<div>
|
|
<h3 className="text-lg font-semibold mb-3">YouTube Results</h3>
|
|
<div className="grid grid-cols-1 gap-2">
|
|
{results.youtube.map((result: any, index: number) => (
|
|
<div
|
|
key={index}
|
|
className="flex items-center gap-4 p-3 rounded-lg hover:bg-accent"
|
|
>
|
|
{result.thumbnail && (
|
|
<img
|
|
src={result.thumbnail}
|
|
alt={result.title}
|
|
className="w-16 h-16 object-cover rounded"
|
|
/>
|
|
)}
|
|
<div className="flex-1">
|
|
<h4 className="font-medium">{result.title}</h4>
|
|
<p className="text-sm text-muted-foreground">{result.artist}</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
size="icon"
|
|
variant="default"
|
|
onClick={() => handlePlayAndDownload(result.url, result.title, result.thumbnail, result.artist)}
|
|
disabled={downloadMutation.isPending}
|
|
>
|
|
<Play className="h-5 w-5" />
|
|
</Button>
|
|
<Button
|
|
size="icon"
|
|
variant="outline"
|
|
onClick={() => handleDownload(result.url, result.title, result.thumbnail, result.artist)}
|
|
disabled={downloadMutation.isPending}
|
|
>
|
|
<Download className="h-5 w-5" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{results.bilibili?.length > 0 && (
|
|
<div>
|
|
<h3 className="text-lg font-semibold mb-3">Bilibili Results</h3>
|
|
<div className="grid grid-cols-1 gap-2">
|
|
{results.bilibili.map((result: any, index: number) => (
|
|
<div
|
|
key={index}
|
|
className="flex items-center gap-4 p-3 rounded-lg hover:bg-accent"
|
|
>
|
|
{result.thumbnail && (
|
|
<img
|
|
src={result.thumbnail}
|
|
alt={result.title}
|
|
className="w-16 h-16 object-cover rounded"
|
|
/>
|
|
)}
|
|
<div className="flex-1">
|
|
<h4 className="font-medium">{result.title}</h4>
|
|
<p className="text-sm text-muted-foreground">{result.artist}</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
size="icon"
|
|
variant="default"
|
|
onClick={() => handlePlayAndDownload(result.url, result.title, result.thumbnail, result.artist)}
|
|
disabled={downloadMutation.isPending}
|
|
>
|
|
<Play className="h-5 w-5" />
|
|
</Button>
|
|
<Button
|
|
size="icon"
|
|
variant="outline"
|
|
onClick={() => handleDownload(result.url, result.title, result.thumbnail, result.artist)}
|
|
disabled={downloadMutation.isPending}
|
|
>
|
|
<Download className="h-5 w-5" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|