mirror of
https://github.com/wahyd4/you-music.git
synced 2026-08-08 20:59:47 +10:00
Set proper thumbnail
This commit is contained in:
@@ -0,0 +1,50 @@
|
|||||||
|
"""Add thumbnail to download_jobs
|
||||||
|
|
||||||
|
Revision ID: dcb7e2f03d2b
|
||||||
|
Revises: 86ec19b8f5ce
|
||||||
|
Create Date: 2025-11-07 20:26:29.893708
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'dcb7e2f03d2b'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = '86ec19b8f5ce'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('download_jobs', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('thumbnail', sa.String(), nullable=True))
|
||||||
|
|
||||||
|
with op.batch_alter_table('music', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_music_created_at'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_music_updated_at'))
|
||||||
|
|
||||||
|
with op.batch_alter_table('playlists', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_playlists_created_at'))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('playlists', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_playlists_created_at'), ['created_at'], unique=False)
|
||||||
|
|
||||||
|
with op.batch_alter_table('music', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_music_updated_at'), ['updated_at'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_music_created_at'), ['created_at'], unique=False)
|
||||||
|
|
||||||
|
with op.batch_alter_table('download_jobs', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('thumbnail')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -69,7 +69,7 @@ async def process_download_job(job_id: int, db: AsyncSession, direct_url: str =
|
|||||||
file_exists=True,
|
file_exists=True,
|
||||||
source_url=direct_url,
|
source_url=direct_url,
|
||||||
source_type=source_type,
|
source_type=source_type,
|
||||||
thumbnail=metadata.get("thumbnail")
|
thumbnail=metadata.get("thumbnail") or job.thumbnail
|
||||||
)
|
)
|
||||||
|
|
||||||
db.add(db_music)
|
db.add(db_music)
|
||||||
@@ -237,7 +237,8 @@ async def create_download_job(
|
|||||||
# Create job
|
# Create job
|
||||||
job = DownloadJob(
|
job = DownloadJob(
|
||||||
song_name=song_name,
|
song_name=song_name,
|
||||||
status="pending"
|
status="pending",
|
||||||
|
thumbnail=request.thumbnail
|
||||||
)
|
)
|
||||||
|
|
||||||
db.add(job)
|
db.add(job)
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ class DownloadJob(Base):
|
|||||||
confirmed = Column(Boolean, default=False) # Whether user confirmed duplicate download
|
confirmed = Column(Boolean, default=False) # Whether user confirmed duplicate download
|
||||||
is_duplicate = Column(Boolean, default=False) # Whether song already exists
|
is_duplicate = Column(Boolean, default=False) # Whether song already exists
|
||||||
duplicate_music_id = Column(Integer, nullable=True) # ID of existing duplicate
|
duplicate_music_id = Column(Integer, nullable=True) # ID of existing duplicate
|
||||||
|
thumbnail = Column(String, nullable=True) # Thumbnail URL from search results
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, index=True)
|
created_at = Column(DateTime, default=datetime.utcnow, index=True)
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ class DownloadJobCreate(BaseModel):
|
|||||||
song_name: str
|
song_name: str
|
||||||
artist: Optional[str] = None # Artist name to improve search accuracy
|
artist: Optional[str] = None # Artist name to improve search accuracy
|
||||||
direct_url: Optional[str] = None # Direct URL to download (skips search)
|
direct_url: Optional[str] = None # Direct URL to download (skips search)
|
||||||
|
thumbnail: Optional[str] = None # Thumbnail URL from search results
|
||||||
|
|
||||||
|
|
||||||
class DownloadJobResponse(BaseModel):
|
class DownloadJobResponse(BaseModel):
|
||||||
@@ -164,6 +165,7 @@ class DownloadJobResponse(BaseModel):
|
|||||||
confirmed: bool
|
confirmed: bool
|
||||||
is_duplicate: bool
|
is_duplicate: bool
|
||||||
duplicate_music_id: Optional[int] = None
|
duplicate_music_id: Optional[int] = None
|
||||||
|
thumbnail: Optional[str] = None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
|
|
||||||
|
|||||||
@@ -175,10 +175,20 @@ class MusicDownloader:
|
|||||||
files = []
|
files = []
|
||||||
for ext in ['.mp3', '.m4a', '.opus', '.webm']:
|
for ext in ['.mp3', '.m4a', '.opus', '.webm']:
|
||||||
if output_name:
|
if output_name:
|
||||||
|
# Try exact match first
|
||||||
pattern = f"{output_name}{ext}"
|
pattern = f"{output_name}{ext}"
|
||||||
file_path = os.path.join(self.download_path, pattern)
|
file_path = os.path.join(self.download_path, pattern)
|
||||||
if os.path.exists(file_path):
|
if os.path.exists(file_path):
|
||||||
return file_path
|
return file_path
|
||||||
|
|
||||||
|
# yt-dlp may sanitize differently (e.g., " -> ')
|
||||||
|
# Try fuzzy match: look for files that start with similar name
|
||||||
|
# Extract base name without special chars for comparison
|
||||||
|
base_search = re.sub(r'[^\w\s-]', '', output_name.lower())
|
||||||
|
for file in Path(self.download_path).glob(f"*{ext}"):
|
||||||
|
file_base = re.sub(r'[^\w\s-]', '', file.stem.lower())
|
||||||
|
if file_base == base_search:
|
||||||
|
return str(file)
|
||||||
else:
|
else:
|
||||||
# Find most recent file
|
# Find most recent file
|
||||||
for file in Path(self.download_path).glob(f"*{ext}"):
|
for file in Path(self.download_path).glob(f"*{ext}"):
|
||||||
|
|||||||
+2
-7
@@ -77,11 +77,11 @@ echo ""
|
|||||||
# 3. Backend Setup
|
# 3. Backend Setup
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
echo "🔧 Setting up Backend..."
|
echo "🔧 Setting up Backend..."
|
||||||
|
cd backend
|
||||||
|
|
||||||
# Create virtual environment if needed
|
# Create virtual environment if needed
|
||||||
if [ ! -d "backend/.venv" ]; then
|
if [ ! -d ".venv" ]; then
|
||||||
echo "Creating Python 3.13 virtual environment..."
|
echo "Creating Python 3.13 virtual environment..."
|
||||||
cd backend
|
|
||||||
uv venv --python 3.13
|
uv venv --python 3.13
|
||||||
cd ..
|
cd ..
|
||||||
fi
|
fi
|
||||||
@@ -91,11 +91,6 @@ echo "Installing Python dependencies with uv..."
|
|||||||
uv sync
|
uv sync
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
# Create backend .env if needed
|
|
||||||
if [ ! -f "backend/.env" ]; then
|
|
||||||
echo "Creating backend .env file..."
|
|
||||||
cp backend/.env.example backend/.env 2>/dev/null || echo "Warning: .env.example not found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${GREEN}✅ Backend setup complete${NC}"
|
echo -e "${GREEN}✅ Backend setup complete${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ export const settingsApi = {
|
|||||||
|
|
||||||
// Auto Download API
|
// Auto Download API
|
||||||
export const autoDownloadApi = {
|
export const autoDownloadApi = {
|
||||||
createJob: (songName: string, artist?: string, directUrl?: string) =>
|
createJob: (songName: string, artist?: string, directUrl?: string, thumbnail?: string) =>
|
||||||
api.post('/auto-download/job', { song_name: songName, artist, direct_url: directUrl }),
|
api.post('/auto-download/job', { song_name: songName, artist, direct_url: directUrl, thumbnail }),
|
||||||
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) =>
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ export default function SearchPage({ onPlayMusic }: SearchPageProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const downloadMutation = useMutation({
|
const downloadMutation = useMutation({
|
||||||
mutationFn: ({ title, artist, url }: { title: string; artist?: string; url?: string }) =>
|
mutationFn: ({ title, artist, url, thumbnail }: { title: string; artist?: string; url?: string; thumbnail?: string }) =>
|
||||||
autoDownloadApi.createJob(title, artist, url),
|
autoDownloadApi.createJob(title, artist, url, thumbnail),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('Download job created! Check Download Center for progress.')
|
toast.success('Download job created! Check Download Center for progress.')
|
||||||
},
|
},
|
||||||
@@ -51,13 +51,13 @@ export default function SearchPage({ onPlayMusic }: SearchPageProps) {
|
|||||||
setSearchQuery(query)
|
setSearchQuery(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDownload = (title: string, artist?: string, url?: string) => {
|
const handleDownload = (title: string, artist?: string, url?: string, thumbnail?: string) => {
|
||||||
downloadMutation.mutate({ title, artist, url })
|
downloadMutation.mutate({ title, artist, url, thumbnail })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePlayAndDownload = async (url: string, title: string, thumbnail?: string, artist?: string) => {
|
const handlePlayAndDownload = async (url: string, title: string, thumbnail?: string, artist?: string) => {
|
||||||
// Start download job with direct URL
|
// Start download job with direct URL
|
||||||
downloadMutation.mutate({ title, artist, url })
|
downloadMutation.mutate({ title, artist, url, thumbnail })
|
||||||
|
|
||||||
// Create temporary music object for streaming playback
|
// Create temporary music object for streaming playback
|
||||||
const streamUrl = `/api/stream?url=${encodeURIComponent(url)}`
|
const streamUrl = `/api/stream?url=${encodeURIComponent(url)}`
|
||||||
@@ -185,7 +185,7 @@ export default function SearchPage({ onPlayMusic }: SearchPageProps) {
|
|||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => handleDownload(result.title, result.artist, result.url)}
|
onClick={() => handleDownload(result.title, result.artist, result.url, result.thumbnail)}
|
||||||
disabled={downloadMutation.isPending}
|
disabled={downloadMutation.isPending}
|
||||||
>
|
>
|
||||||
<Download className="h-5 w-5" />
|
<Download className="h-5 w-5" />
|
||||||
@@ -229,7 +229,7 @@ export default function SearchPage({ onPlayMusic }: SearchPageProps) {
|
|||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => handleDownload(result.title, result.artist, result.url)}
|
onClick={() => handleDownload(result.title, result.artist, result.url, result.thumbnail)}
|
||||||
disabled={downloadMutation.isPending}
|
disabled={downloadMutation.isPending}
|
||||||
>
|
>
|
||||||
<Download className="h-5 w-5" />
|
<Download className="h-5 w-5" />
|
||||||
|
|||||||
Reference in New Issue
Block a user