mirror of
https://github.com/wahyd4/you-music.git
synced 2026-08-09 05:06:44 +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 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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', ''),
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user