diff --git a/backend/app/api/auto_download.py b/backend/app/api/auto_download.py index 6097547..2aab85e 100644 --- a/backend/app/api/auto_download.py +++ b/backend/app/api/auto_download.py @@ -18,7 +18,7 @@ router = APIRouter() logger = logging.getLogger(__name__) -async def process_download_job(job_id: int, db: AsyncSession): +async def process_download_job(job_id: int, db: AsyncSession, direct_url: str = None): """Background task to process a download job""" try: # Get job @@ -27,6 +27,69 @@ async def process_download_job(job_id: int, db: AsyncSession): if not job: return + # Check if this is a direct URL download (skip search) + if direct_url: + logger.info(f"Direct URL download job {job_id}: url='{direct_url}', song_name='{job.song_name}'") + + # Skip search, go directly to download + job.status = "downloading" + await db.commit() + + # Format output filename + output_filename = job.song_name # Already formatted as "Artist - Title" + + # Download the music + success, file_path, error = await music_downloader.download_music( + direct_url, + output_filename + ) + + if success and file_path: + # Extract metadata + metadata = await music_downloader.get_music_metadata(file_path) + + # Determine source type + source_type = "youtube" if music_downloader.is_youtube_url(direct_url) else \ + "bilibili" if music_downloader.is_bilibili_url(direct_url) else "other" + + # Get relative path and file format + relative_path = os.path.relpath(file_path, settings.MUSIC_DIR) + file_extension = os.path.splitext(file_path)[1][1:] + + # Create database record + db_music = Music( + title=metadata.get("title", job.song_name or "Unknown"), + artist=metadata.get("artist", "Unknown"), + album=metadata.get("album", ""), + duration=metadata.get("duration", 0), + file_path=relative_path, + file_size=os.path.getsize(file_path), + file_format=file_extension, + file_location=file_path, + file_exists=True, + source_url=direct_url, + source_type=source_type, + thumbnail=metadata.get("thumbnail") + ) + + db.add(db_music) + await db.commit() + await db.refresh(db_music) + + job.status = "completed" + job.music_id = db_music.id + await db.commit() + + logger.info(f"Direct download job {job_id} completed: music_id={db_music.id}") + else: + job.status = "failed" + job.error_message = error or "Download failed" + await db.commit() + logger.error(f"Direct download job {job_id} failed: {error}") + + return + + # Original auto-download flow (with search) # Update status to searching job.status = "searching" await db.commit() @@ -159,11 +222,17 @@ async def create_download_job( # Format song_name to include artist if provided # This ensures better search results song_name = request.song_name - if request.artist: - # Format as "Artist - Song" for better search accuracy + if request.artist and not request.direct_url: + # Format as "Artist - Song" for better search accuracy (only if not direct URL) + song_name = f"{request.artist} - {request.song_name}" + elif request.artist and request.direct_url: + # For direct URL, still format the output filename song_name = f"{request.artist} - {request.song_name}" - logger.info(f"Creating auto-download job: song_name='{song_name}' (original: '{request.song_name}', artist: '{request.artist}')") + if request.direct_url: + logger.info(f"Creating direct download job: url='{request.direct_url}', filename='{song_name}'") + else: + logger.info(f"Creating auto-download job: song_name='{song_name}' (original: '{request.song_name}', artist: '{request.artist}')") # Create job job = DownloadJob( @@ -176,7 +245,8 @@ async def create_download_job( await db.refresh(job) # Start processing in background - background_tasks.add_task(process_download_job, job.id, db) + # Pass direct_url if provided to skip search + background_tasks.add_task(process_download_job, job.id, db, request.direct_url) return job diff --git a/backend/app/schemas/schemas.py b/backend/app/schemas/schemas.py index 03b3f16..47eb1a3 100644 --- a/backend/app/schemas/schemas.py +++ b/backend/app/schemas/schemas.py @@ -148,6 +148,7 @@ class MusicDetailInfo(BaseModel): class DownloadJobCreate(BaseModel): song_name: str artist: Optional[str] = None # Artist name to improve search accuracy + direct_url: Optional[str] = None # Direct URL to download (skips search) class DownloadJobResponse(BaseModel): diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 0d4ff06..93622e4 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -100,8 +100,8 @@ export const settingsApi = { // Auto Download API export const autoDownloadApi = { - createJob: (songName: string, artist?: string) => - api.post('/auto-download/job', { song_name: songName, artist }), + createJob: (songName: string, artist?: string, directUrl?: string) => + api.post('/auto-download/job', { song_name: songName, artist, direct_url: directUrl }), getJobs: (status?: string) => api.get('/auto-download/jobs', { params: { status } }), getJob: (jobId: number) => diff --git a/frontend/src/components/search/SearchPage.tsx b/frontend/src/components/search/SearchPage.tsx index 3ad59cb..8073451 100644 --- a/frontend/src/components/search/SearchPage.tsx +++ b/frontend/src/components/search/SearchPage.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { searchApi, downloadApi, musicApi } from '@/api/client' +import { useQuery, useMutation } from '@tanstack/react-query' +import { searchApi, autoDownloadApi, musicApi } from '@/api/client' import { Music } from '@/types' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' @@ -14,7 +14,6 @@ interface SearchPageProps { export default function SearchPage({ onPlayMusic }: SearchPageProps) { const [query, setQuery] = useState('') const [searchQuery, setSearchQuery] = useState('') - const queryClient = useQueryClient() const { data: localResults, isLoading: isLoadingLocal } = useQuery({ queryKey: ['music', 'search', searchQuery], @@ -37,14 +36,13 @@ export default function SearchPage({ onPlayMusic }: SearchPageProps) { }) const downloadMutation = useMutation({ - mutationFn: ({ url, title, thumbnail, artist }: { url: string; title: string; thumbnail?: string; artist?: string }) => - downloadApi.downloadMusic({ url, title, thumbnail, artist }), + mutationFn: ({ title, artist, url }: { title: string; artist?: string; url?: string }) => + autoDownloadApi.createJob(title, artist, url), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['download-status'] }) - toast.success('Download started! Check Download Center for progress.') + toast.success('Download job created! Check Download Center for progress.') }, onError: () => { - toast.error('Download failed') + toast.error('Failed to create download job') }, }) @@ -53,14 +51,14 @@ export default function SearchPage({ onPlayMusic }: SearchPageProps) { setSearchQuery(query) } - const handleDownload = (url: string, title: string, thumbnail?: string, artist?: string) => { - downloadMutation.mutate({ url, title, thumbnail, artist }) + const handleDownload = (title: string, artist?: string, url?: string) => { + downloadMutation.mutate({ title, artist, url }) } const handlePlayAndDownload = async (url: string, title: string, thumbnail?: string, artist?: string) => { - // Start download - downloadMutation.mutate({ url, title, thumbnail, artist }) - + // Start download job with direct URL + downloadMutation.mutate({ title, artist, url }) + // Create temporary music object for streaming playback const streamUrl = `/api/stream?url=${encodeURIComponent(url)}` const tempMusic: Music = { @@ -83,7 +81,7 @@ export default function SearchPage({ onPlayMusic }: SearchPageProps) { file_exists: true, last_scanned_at: null, } - + onPlayMusic(tempMusic) toast.success('Playing while downloading...') } @@ -187,7 +185,7 @@ export default function SearchPage({ onPlayMusic }: SearchPageProps) {