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