diff --git a/backend/app/api/auto_download.py b/backend/app/api/auto_download.py index 8f02b6c..6097547 100644 --- a/backend/app/api/auto_download.py +++ b/backend/app/api/auto_download.py @@ -3,6 +3,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from typing import List, Optional import json +import logging from app.db.session import get_db from app.models.models import DownloadJob, Music @@ -14,6 +15,7 @@ import os from datetime import datetime router = APIRouter() +logger = logging.getLogger(__name__) async def process_download_job(job_id: int, db: AsyncSession): @@ -30,6 +32,8 @@ async def process_download_job(job_id: int, db: AsyncSession): await db.commit() # Search for the song + # If artist is included in song_name (format: "Artist - Song"), use it directly + # Otherwise, just search by song name search_results = await music_searcher.search_all(job.song_name, limit=10) # Combine results and filter out short clips (< 90 seconds) to avoid samples @@ -86,6 +90,8 @@ async def process_download_job(job_id: int, db: AsyncSession): # Format output filename as "artist - title" to avoid downloading wrong songs output_filename = f"{selected_result.artist} - {selected_result.title}" if selected_result.artist else selected_result.title + logger.info(f"Auto-download job {job_id}: artist='{selected_result.artist}', title='{selected_result.title}', output_filename='{output_filename}'") + # Download the music success, file_path, error = await music_downloader.download_music( selected_result.url, @@ -150,9 +156,18 @@ async def create_download_job( db: AsyncSession = Depends(get_db) ): """Create a new auto-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 + 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}')") + # Create job job = DownloadJob( - song_name=request.song_name, + song_name=song_name, status="pending" ) diff --git a/backend/app/api/download.py b/backend/app/api/download.py index a962d0b..6d6c2e8 100644 --- a/backend/app/api/download.py +++ b/backend/app/api/download.py @@ -1,6 +1,7 @@ from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select +import logging from app.db.session import get_db from app.models.models import Music @@ -13,6 +14,7 @@ import os import uuid router = APIRouter() +logger = logging.getLogger(__name__) async def process_download( @@ -29,6 +31,8 @@ async def process_download( # Format output filename as "artist - title" if artist is provided output_filename = f"{artist} - {title}" if artist else title + logger.info(f"Download task {task_id}: artist='{artist}', title='{title}', output_filename='{output_filename}'") + success, file_path, error = await music_downloader.download_music(url, output_filename) if success and file_path: diff --git a/backend/app/schemas/schemas.py b/backend/app/schemas/schemas.py index 3bb2fe8..03b3f16 100644 --- a/backend/app/schemas/schemas.py +++ b/backend/app/schemas/schemas.py @@ -147,6 +147,7 @@ class MusicDetailInfo(BaseModel): class DownloadJobCreate(BaseModel): song_name: str + artist: Optional[str] = None # Artist name to improve search accuracy class DownloadJobResponse(BaseModel): diff --git a/backend/app/services/search.py b/backend/app/services/search.py index bdc4e56..be4ad46 100644 --- a/backend/app/services/search.py +++ b/backend/app/services/search.py @@ -3,10 +3,58 @@ import re from typing import List, Optional from app.schemas.schemas import SearchResult import logging +import asyncio logger = logging.getLogger(__name__) +def extract_artist_from_title(title: str) -> tuple[str, str]: + """ + Try to extract artist and song name from YouTube video title. + Common patterns: + - "Artist - Song Title" + - "Artist: Song Title" + - "Song Title by Artist" + - "Artist | Song Title" + + Returns: (artist, clean_title) + """ + # Pattern 1: "Artist - Song Title" + match = re.match(r'^([^-]+)\s*-\s*(.+)$', title) + if match: + artist = match.group(1).strip() + song = match.group(2).strip() + # Remove common suffixes like (Official Video), [Official Audio], etc. + song = re.sub(r'\s*[\(\[].*?(official|audio|video|lyric|mv).*?[\)\]]', '', song, flags=re.IGNORECASE) + return artist, song.strip() + + # Pattern 2: "Artist: Song Title" + match = re.match(r'^([^:]+):\s*(.+)$', title) + if match: + artist = match.group(1).strip() + song = match.group(2).strip() + song = re.sub(r'\s*[\(\[].*?(official|audio|video|lyric|mv).*?[\)\]]', '', song, flags=re.IGNORECASE) + return artist, song.strip() + + # Pattern 3: "Song Title by Artist" + match = re.search(r'^(.+?)\s+by\s+([^(\[]+)', title, re.IGNORECASE) + if match: + song = match.group(1).strip() + artist = match.group(2).strip() + return artist, song + + # Pattern 4: "Artist | Song Title" + match = re.match(r'^([^|]+)\|\s*(.+)$', title) + if match: + artist = match.group(1).strip() + song = match.group(2).strip() + song = re.sub(r'\s*[\(\[].*?(official|audio|video|lyric|mv).*?[\)\]]', '', song, flags=re.IGNORECASE) + return artist, song.strip() + + # No pattern matched, return empty artist and original title + return "", title + + class MusicSearcher: """Search for music from various sources (similar to xiaomusic search logic)""" @@ -44,9 +92,29 @@ class MusicSearcher: for line in lines: try: data = json.loads(line) + title = data.get('title', '') + + # Try to get artist from metadata first + artist = ( + data.get('artist') or + data.get('creator') or + '' + ) + + # If no artist in metadata, try to extract from title + if not artist: + extracted_artist, clean_title = extract_artist_from_title(title) + if extracted_artist: + artist = extracted_artist + title = clean_title + + # If still no artist, use uploader as last resort + if not artist: + artist = data.get('uploader', '') + results.append(SearchResult( - title=data.get('title', ''), - artist=data.get('uploader', ''), + title=title, + artist=artist, duration=data.get('duration', 0), thumbnail=data.get('thumbnail', ''), url=data.get('webpage_url', ''), @@ -94,9 +162,19 @@ class MusicSearcher: if data.get('code') == 0 and 'data' in data: for item in data['data'].get('result', []): + title = item.get('title', '').replace('', '').replace('', '') + artist = item.get('author', '') + + # If no artist, try to extract from title + if not artist: + extracted_artist, clean_title = extract_artist_from_title(title) + if extracted_artist: + artist = extracted_artist + title = clean_title + results.append(SearchResult( - title=item.get('title', '').replace('', '').replace('', ''), - artist=item.get('author', ''), + title=title, + artist=artist, duration=item.get('duration', 0), thumbnail=f"https:{item.get('pic', '')}" if item.get('pic') else '', url=item.get('arcurl', ''), diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 8b61810..0d4ff06 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) => - api.post('/auto-download/job', { song_name: songName }), + createJob: (songName: string, artist?: string) => + api.post('/auto-download/job', { song_name: songName, artist }), getJobs: (status?: string) => api.get('/auto-download/jobs', { params: { status } }), getJob: (jobId: number) => diff --git a/frontend/src/components/artist/ArtistDetailPage.tsx b/frontend/src/components/artist/ArtistDetailPage.tsx index bd79ff4..547f3c5 100644 --- a/frontend/src/components/artist/ArtistDetailPage.tsx +++ b/frontend/src/components/artist/ArtistDetailPage.tsx @@ -68,8 +68,8 @@ export default function ArtistDetailPage({ onPlayMusic }: ArtistDetailProps) { }) const autoDownloadMutation = useMutation({ - mutationFn: async (songName: string) => { - const response = await autoDownloadApi.createJob(songName) + mutationFn: async ({ songTitle, artist }: { songTitle: string; artist: string }) => { + const response = await autoDownloadApi.createJob(songTitle, artist) return response.data }, onSuccess: () => { @@ -362,7 +362,10 @@ export default function ArtistDetailPage({ onPlayMusic }: ArtistDetailProps) { Cancel