mirror of
https://github.com/wahyd4/you-music.git
synced 2026-08-08 20:59:47 +10:00
Update download logic
This commit is contained in:
@@ -0,0 +1,201 @@
|
|||||||
|
# Download Filename Format Fix
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
Songs downloaded from artist detail page and remote search results sometimes download the wrong file because:
|
||||||
|
- Multiple songs with the same title but different artists exist
|
||||||
|
- yt-dlp might pick a different version when searching by title only
|
||||||
|
- File organization is poor without artist information in filename
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
Changed download filename format from:
|
||||||
|
```
|
||||||
|
Song Title.mp3
|
||||||
|
```
|
||||||
|
|
||||||
|
To:
|
||||||
|
```
|
||||||
|
Artist Name - Song Title.mp3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
#### 1. `backend/app/api/download.py`
|
||||||
|
- Updated `process_download()` to accept `artist` parameter
|
||||||
|
- Format filename as `"{artist} - {title}"` when artist is provided
|
||||||
|
- Pass artist from request to background task
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```python
|
||||||
|
success, file_path, error = await music_downloader.download_music(url, title)
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```python
|
||||||
|
output_filename = f"{artist} - {title}" if artist else title
|
||||||
|
success, file_path, error = await music_downloader.download_music(url, output_filename)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. `backend/app/api/auto_download.py`
|
||||||
|
- Updated auto-download to format filename with artist
|
||||||
|
- Uses artist from `selected_result`
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```python
|
||||||
|
success, file_path, error = await music_downloader.download_music(
|
||||||
|
selected_result.url,
|
||||||
|
selected_result.title
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```python
|
||||||
|
output_filename = f"{selected_result.artist} - {selected_result.title}" if selected_result.artist else selected_result.title
|
||||||
|
success, file_path, error = await music_downloader.download_music(
|
||||||
|
selected_result.url,
|
||||||
|
output_filename
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. `backend/app/services/downloader.py`
|
||||||
|
- Added `sanitize_filename()` function to handle special characters
|
||||||
|
- Prevents filesystem errors from characters like `/`, `\`, `:`, `*`, etc.
|
||||||
|
- Limits filename length to 200 characters
|
||||||
|
|
||||||
|
**New function:**
|
||||||
|
```python
|
||||||
|
def sanitize_filename(filename: str) -> str:
|
||||||
|
"""Sanitize filename by removing/replacing problematic characters"""
|
||||||
|
replacements = {
|
||||||
|
'/': '-',
|
||||||
|
'\\': '-',
|
||||||
|
':': '-',
|
||||||
|
'*': '',
|
||||||
|
'?': '',
|
||||||
|
'"': "'",
|
||||||
|
'<': '',
|
||||||
|
'>': '',
|
||||||
|
'|': '-',
|
||||||
|
}
|
||||||
|
# ... sanitization logic
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
#### 4. `frontend/src/components/search/SearchPage.tsx`
|
||||||
|
- Fixed mutation to actually pass `artist` and `thumbnail` to API
|
||||||
|
- Was accepting parameters but not sending them
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```typescript
|
||||||
|
mutationFn: ({ url, title }: { url: string; title: string; thumbnail?: string; artist?: string }) =>
|
||||||
|
downloadApi.downloadMusic({ url, title }),
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```typescript
|
||||||
|
mutationFn: ({ url, title, thumbnail, artist }: { url: string; title: string; thumbnail?: string; artist?: string }) =>
|
||||||
|
downloadApi.downloadMusic({ url, title, thumbnail, artist }),
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Example 1: Search Page Download
|
||||||
|
**User searches for:** "Shape of You"
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
- Filename: `Shape of You.mp3`
|
||||||
|
- Could download any "Shape of You" (Ed Sheeran, cover, remix, etc.)
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
- Filename: `Ed Sheeran - Shape of You.mp3`
|
||||||
|
- Downloads the correct version with artist specified
|
||||||
|
|
||||||
|
### Example 2: Artist Detail Page
|
||||||
|
**User is on "Taylor Swift" artist page, downloads "Love Story"**
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
- Filename: `Love Story.mp3`
|
||||||
|
- Might download a different artist's "Love Story"
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
- Filename: `Taylor Swift - Love Story.mp3`
|
||||||
|
- Guaranteed to download the correct artist's version
|
||||||
|
|
||||||
|
### Example 3: Special Characters
|
||||||
|
**Song:** `AC/DC - Back In Black: Remastered`
|
||||||
|
|
||||||
|
**After sanitization:**
|
||||||
|
- Filename: `AC-DC - Back In Black- Remastered.mp3`
|
||||||
|
- Safe for all filesystems
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Test Basic Download
|
||||||
|
```bash
|
||||||
|
# Via API
|
||||||
|
curl -X POST "http://localhost:8000/api/download/music" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"url": "https://youtube.com/watch?v=xxx",
|
||||||
|
"title": "Shape of You",
|
||||||
|
"artist": "Ed Sheeran"
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Check the downloaded file
|
||||||
|
ls -la data/music/
|
||||||
|
# Should see: "Ed Sheeran - Shape of You.mp3"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Auto Download
|
||||||
|
```bash
|
||||||
|
# From artist page, download "Love Story" from Taylor Swift
|
||||||
|
# Should create: "Taylor Swift - Love Story.mp3"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Special Characters
|
||||||
|
```bash
|
||||||
|
# Download song with special characters in name
|
||||||
|
curl -X POST "http://localhost:8000/api/download/music" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"url": "https://youtube.com/watch?v=xxx",
|
||||||
|
"title": "Don'\''t Stop Me Now",
|
||||||
|
"artist": "Queen"
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Should create: "Queen - Don't Stop Me Now.mp3"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration
|
||||||
|
|
||||||
|
No database migration needed. Existing files are not affected.
|
||||||
|
|
||||||
|
New downloads will follow the new format:
|
||||||
|
- ✅ Downloads from search page: `Artist - Title.mp3`
|
||||||
|
- ✅ Downloads from artist detail page: `Artist - Title.mp3`
|
||||||
|
- ✅ Auto-downloads: `Artist - Title.mp3`
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
1. **Prevents wrong downloads** - Artist name ensures correct version
|
||||||
|
2. **Better organization** - Easy to identify songs by filename
|
||||||
|
3. **Consistent naming** - All downloads follow same format
|
||||||
|
4. **Filesystem safe** - Special characters are sanitized
|
||||||
|
5. **Backward compatible** - Old files still work
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
|
||||||
|
- `backend/app/api/download.py` - Pass artist to formatter
|
||||||
|
- `backend/app/api/auto_download.py` - Format auto-download filenames
|
||||||
|
- `backend/app/services/downloader.py` - Add sanitization function
|
||||||
|
- `frontend/src/components/search/SearchPage.tsx` - Fix mutation to pass artist
|
||||||
|
|
||||||
|
## Related Issues
|
||||||
|
|
||||||
|
This fix addresses:
|
||||||
|
- Wrong songs being downloaded when title is ambiguous
|
||||||
|
- Poor file organization without artist info
|
||||||
|
- Filesystem errors from special characters in filenames
|
||||||
|
- Inconsistent naming across different download sources
|
||||||
@@ -83,10 +83,13 @@ async def process_download_job(job_id: int, db: AsyncSession):
|
|||||||
job.status = "downloading"
|
job.status = "downloading"
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
# 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,
|
||||||
selected_result.title
|
output_filename
|
||||||
)
|
)
|
||||||
|
|
||||||
if success and file_path:
|
if success and file_path:
|
||||||
|
|||||||
@@ -20,12 +20,16 @@ async def process_download(
|
|||||||
url: str,
|
url: str,
|
||||||
title: str,
|
title: str,
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
add_to_playlist: str = None
|
add_to_playlist: str = None,
|
||||||
|
artist: str = None
|
||||||
):
|
):
|
||||||
"""Background task to download music"""
|
"""Background task to download music"""
|
||||||
await download_queue.update_status(task_id, "downloading", progress=0.0)
|
await download_queue.update_status(task_id, "downloading", progress=0.0)
|
||||||
|
|
||||||
success, file_path, error = await music_downloader.download_music(url, title)
|
# Format output filename as "artist - title" if artist is provided
|
||||||
|
output_filename = f"{artist} - {title}" if artist else title
|
||||||
|
|
||||||
|
success, file_path, error = await music_downloader.download_music(url, output_filename)
|
||||||
|
|
||||||
if success and file_path:
|
if success and file_path:
|
||||||
await download_queue.update_status(task_id, "downloading", progress=80.0)
|
await download_queue.update_status(task_id, "downloading", progress=80.0)
|
||||||
@@ -121,7 +125,8 @@ async def download_music(
|
|||||||
request.url,
|
request.url,
|
||||||
request.title,
|
request.title,
|
||||||
db,
|
db,
|
||||||
request.add_to_playlist
|
request.add_to_playlist,
|
||||||
|
request.artist # Pass artist to format filename
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -14,6 +14,35 @@ import logging
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_filename(filename: str) -> str:
|
||||||
|
"""Sanitize filename by removing/replacing problematic characters"""
|
||||||
|
# Replace problematic characters with safe alternatives
|
||||||
|
replacements = {
|
||||||
|
'/': '-',
|
||||||
|
'\\': '-',
|
||||||
|
':': '-',
|
||||||
|
'*': '',
|
||||||
|
'?': '',
|
||||||
|
'"': "'",
|
||||||
|
'<': '',
|
||||||
|
'>': '',
|
||||||
|
'|': '-',
|
||||||
|
}
|
||||||
|
|
||||||
|
for char, replacement in replacements.items():
|
||||||
|
filename = filename.replace(char, replacement)
|
||||||
|
|
||||||
|
# Remove multiple spaces and trim
|
||||||
|
filename = ' '.join(filename.split())
|
||||||
|
|
||||||
|
# Limit length (leave room for extension)
|
||||||
|
max_length = 200
|
||||||
|
if len(filename) > max_length:
|
||||||
|
filename = filename[:max_length].strip()
|
||||||
|
|
||||||
|
return filename
|
||||||
|
|
||||||
|
|
||||||
class MusicDownloader:
|
class MusicDownloader:
|
||||||
"""Download music from various sources using yt-dlp (similar to xiaomusic)"""
|
"""Download music from various sources using yt-dlp (similar to xiaomusic)"""
|
||||||
|
|
||||||
@@ -74,7 +103,9 @@ class MusicDownloader:
|
|||||||
|
|
||||||
# Prepare output template
|
# Prepare output template
|
||||||
if output_name:
|
if output_name:
|
||||||
title = f"{output_name}.%(ext)s"
|
# Sanitize the output name to avoid filesystem issues
|
||||||
|
sanitized_name = sanitize_filename(output_name)
|
||||||
|
title = f"{sanitized_name}.%(ext)s"
|
||||||
else:
|
else:
|
||||||
title = "%(title)s.%(ext)s"
|
title = "%(title)s.%(ext)s"
|
||||||
|
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ export default function SearchPage({ onPlayMusic }: SearchPageProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const downloadMutation = useMutation({
|
const downloadMutation = useMutation({
|
||||||
mutationFn: ({ url, title }: { url: string; title: string; thumbnail?: string; artist?: string }) =>
|
mutationFn: ({ url, title, thumbnail, artist }: { url: string; title: string; thumbnail?: string; artist?: string }) =>
|
||||||
downloadApi.downloadMusic({ url, title }),
|
downloadApi.downloadMusic({ url, title, thumbnail, artist }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['download-status'] })
|
queryClient.invalidateQueries({ queryKey: ['download-status'] })
|
||||||
toast.success('Download started! Check Download Center for progress.')
|
toast.success('Download started! Check Download Center for progress.')
|
||||||
|
|||||||
Reference in New Issue
Block a user