direct download via youtube links

This commit is contained in:
2026-07-22 09:53:27 -04:00
parent 518b48e395
commit e5643590ee
4 changed files with 106 additions and 2 deletions

View File

@@ -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',

View File

@@ -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">