direct download via youtube links
This commit is contained in:
@@ -17,6 +17,12 @@ export async function searchYouTube(query: string): Promise<YouTubeResult[]> {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function resolveYoutubeUrl(url: string): Promise<YouTubeResult> {
|
||||
const res = await fetch(`${BASE}/youtube/resolve?url=${encodeURIComponent(url)}`);
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function startDownload(req: DownloadRequest): Promise<{ jobId: string }> {
|
||||
const res = await fetch(`${BASE}/download`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Loader2, Play, CheckCircle2, Youtube } from 'lucide-react';
|
||||
import { searchYouTube, startDownload, getDownloadStatus } from '../api/client';
|
||||
import { Loader2, Play, CheckCircle2, Youtube, Link2 } from 'lucide-react';
|
||||
import { searchYouTube, resolveYoutubeUrl, startDownload, getDownloadStatus } from '../api/client';
|
||||
import type { MusicResult, YouTubeResult, DownloadStatus } from '../api/types';
|
||||
|
||||
interface Props {
|
||||
@@ -17,6 +17,10 @@ export default function YouTubePanel({ track, onDownloadDone }: Props) {
|
||||
const [jobStatus, setJobStatus] = useState<DownloadStatus | null>(null);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
|
||||
const [urlInput, setUrlInput] = useState('');
|
||||
const [resolving, setResolving] = useState(false);
|
||||
const [urlError, setUrlError] = useState('');
|
||||
|
||||
// Search YouTube when track changes
|
||||
useEffect(() => {
|
||||
setResults([]);
|
||||
@@ -25,6 +29,8 @@ export default function YouTubePanel({ track, onDownloadDone }: Props) {
|
||||
setJobStatus(null);
|
||||
setLoading(true);
|
||||
setError('');
|
||||
setUrlInput('');
|
||||
setUrlError('');
|
||||
|
||||
const query = `${track.artistName} ${track.trackName} official audio`;
|
||||
searchYouTube(query)
|
||||
@@ -54,6 +60,24 @@ export default function YouTubePanel({ track, onDownloadDone }: Props) {
|
||||
return () => clearInterval(interval);
|
||||
}, [jobId]);
|
||||
|
||||
async function handleResolveUrl(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!urlInput.trim()) return;
|
||||
setResolving(true);
|
||||
setUrlError('');
|
||||
try {
|
||||
const video = await resolveYoutubeUrl(urlInput.trim());
|
||||
setSelectedVideo(video);
|
||||
setJobId(null);
|
||||
setJobStatus(null);
|
||||
setUrlInput('');
|
||||
} catch {
|
||||
setUrlError("Couldn't load that URL — check it's a valid YouTube link.");
|
||||
} finally {
|
||||
setResolving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDownload() {
|
||||
if (!selectedVideo) return;
|
||||
setDownloading(true);
|
||||
@@ -127,6 +151,25 @@ export default function YouTubePanel({ track, onDownloadDone }: Props) {
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Paste a YouTube URL directly */}
|
||||
<p className="section-label">Or paste a YouTube URL</p>
|
||||
<form onSubmit={handleResolveUrl} className="search-form">
|
||||
<div className="search-input-wrap">
|
||||
<Link2 size={18} className="search-icon" />
|
||||
<input
|
||||
className="search-input"
|
||||
type="text"
|
||||
placeholder="https://www.youtube.com/watch?v=…"
|
||||
value={urlInput}
|
||||
onChange={e => setUrlInput(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button className="btn-primary" type="submit" disabled={resolving}>
|
||||
{resolving ? <Loader2 size={16} className="spin" /> : 'Load'}
|
||||
</button>
|
||||
</form>
|
||||
{urlError && <p className="error-msg">{urlError}</p>}
|
||||
|
||||
{/* Embedded player */}
|
||||
{selectedVideo && (
|
||||
<div className="yt-player-wrap">
|
||||
|
||||
Reference in New Issue
Block a user