Update download logic to include artist name

This commit is contained in:
2025-11-07 13:57:07 +11:00
parent 0cb4d9bc94
commit fb8b83657a
6 changed files with 111 additions and 10 deletions
+16 -1
View File
@@ -3,6 +3,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
from typing import List, Optional from typing import List, Optional
import json import json
import logging
from app.db.session import get_db from app.db.session import get_db
from app.models.models import DownloadJob, Music from app.models.models import DownloadJob, Music
@@ -14,6 +15,7 @@ import os
from datetime import datetime from datetime import datetime
router = APIRouter() 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):
@@ -30,6 +32,8 @@ async def process_download_job(job_id: int, db: AsyncSession):
await db.commit() await db.commit()
# Search for the song # 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) search_results = await music_searcher.search_all(job.song_name, limit=10)
# Combine results and filter out short clips (< 90 seconds) to avoid samples # 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 # 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 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 # Download the music
success, file_path, error = await music_downloader.download_music( success, file_path, error = await music_downloader.download_music(
selected_result.url, selected_result.url,
@@ -150,9 +156,18 @@ async def create_download_job(
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Create a new auto-download job""" """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 # Create job
job = DownloadJob( job = DownloadJob(
song_name=request.song_name, song_name=song_name,
status="pending" status="pending"
) )
+4
View File
@@ -1,6 +1,7 @@
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
import logging
from app.db.session import get_db from app.db.session import get_db
from app.models.models import Music from app.models.models import Music
@@ -13,6 +14,7 @@ import os
import uuid import uuid
router = APIRouter() router = APIRouter()
logger = logging.getLogger(__name__)
async def process_download( async def process_download(
@@ -29,6 +31,8 @@ async def process_download(
# Format output filename as "artist - title" if artist is provided # Format output filename as "artist - title" if artist is provided
output_filename = f"{artist} - {title}" if artist else title 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) success, file_path, error = await music_downloader.download_music(url, output_filename)
if success and file_path: if success and file_path:
+1
View File
@@ -147,6 +147,7 @@ class MusicDetailInfo(BaseModel):
class DownloadJobCreate(BaseModel): class DownloadJobCreate(BaseModel):
song_name: str song_name: str
artist: Optional[str] = None # Artist name to improve search accuracy
class DownloadJobResponse(BaseModel): class DownloadJobResponse(BaseModel):
+82 -4
View File
@@ -3,10 +3,58 @@ import re
from typing import List, Optional from typing import List, Optional
from app.schemas.schemas import SearchResult from app.schemas.schemas import SearchResult
import logging import logging
import asyncio
logger = logging.getLogger(__name__) 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: class MusicSearcher:
"""Search for music from various sources (similar to xiaomusic search logic)""" """Search for music from various sources (similar to xiaomusic search logic)"""
@@ -44,9 +92,29 @@ class MusicSearcher:
for line in lines: for line in lines:
try: try:
data = json.loads(line) 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( results.append(SearchResult(
title=data.get('title', ''), title=title,
artist=data.get('uploader', ''), artist=artist,
duration=data.get('duration', 0), duration=data.get('duration', 0),
thumbnail=data.get('thumbnail', ''), thumbnail=data.get('thumbnail', ''),
url=data.get('webpage_url', ''), url=data.get('webpage_url', ''),
@@ -94,9 +162,19 @@ class MusicSearcher:
if data.get('code') == 0 and 'data' in data: if data.get('code') == 0 and 'data' in data:
for item in data['data'].get('result', []): for item in data['data'].get('result', []):
title = item.get('title', '').replace('<em class="keyword">', '').replace('</em>', '')
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( results.append(SearchResult(
title=item.get('title', '').replace('<em class="keyword">', '').replace('</em>', ''), title=title,
artist=item.get('author', ''), artist=artist,
duration=item.get('duration', 0), duration=item.get('duration', 0),
thumbnail=f"https:{item.get('pic', '')}" if item.get('pic') else '', thumbnail=f"https:{item.get('pic', '')}" if item.get('pic') else '',
url=item.get('arcurl', ''), url=item.get('arcurl', ''),
+2 -2
View File
@@ -100,8 +100,8 @@ export const settingsApi = {
// Auto Download API // Auto Download API
export const autoDownloadApi = { export const autoDownloadApi = {
createJob: (songName: string) => createJob: (songName: string, artist?: string) =>
api.post('/auto-download/job', { song_name: songName }), api.post('/auto-download/job', { song_name: songName, artist }),
getJobs: (status?: string) => getJobs: (status?: string) =>
api.get('/auto-download/jobs', { params: { status } }), api.get('/auto-download/jobs', { params: { status } }),
getJob: (jobId: number) => getJob: (jobId: number) =>
@@ -68,8 +68,8 @@ export default function ArtistDetailPage({ onPlayMusic }: ArtistDetailProps) {
}) })
const autoDownloadMutation = useMutation({ const autoDownloadMutation = useMutation({
mutationFn: async (songName: string) => { mutationFn: async ({ songTitle, artist }: { songTitle: string; artist: string }) => {
const response = await autoDownloadApi.createJob(songName) const response = await autoDownloadApi.createJob(songTitle, artist)
return response.data return response.data
}, },
onSuccess: () => { onSuccess: () => {
@@ -362,7 +362,10 @@ export default function ArtistDetailPage({ onPlayMusic }: ArtistDetailProps) {
Cancel Cancel
</Button> </Button>
<Button <Button
onClick={() => selectedSong && autoDownloadMutation.mutate(selectedSong.title)} onClick={() => selectedSong && autoDownloadMutation.mutate({
songTitle: selectedSong.title,
artist: selectedSong.artist
})}
disabled={autoDownloadMutation.isPending} disabled={autoDownloadMutation.isPending}
> >
{autoDownloadMutation.isPending ? ( {autoDownloadMutation.isPending ? (