mirror of
https://github.com/wahyd4/you-music.git
synced 2026-08-08 20:59:47 +10:00
Update download logic to include artist name
This commit is contained in:
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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('<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(
|
||||
title=item.get('title', '').replace('<em class="keyword">', '').replace('</em>', ''),
|
||||
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', ''),
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => selectedSong && autoDownloadMutation.mutate(selectedSong.title)}
|
||||
onClick={() => selectedSong && autoDownloadMutation.mutate({
|
||||
songTitle: selectedSong.title,
|
||||
artist: selectedSong.artist
|
||||
})}
|
||||
disabled={autoDownloadMutation.isPending}
|
||||
>
|
||||
{autoDownloadMutation.isPending ? (
|
||||
|
||||
Reference in New Issue
Block a user