diff --git a/backend/alembic/versions/dcb7e2f03d2b_add_thumbnail_to_download_jobs.py b/backend/alembic/versions/dcb7e2f03d2b_add_thumbnail_to_download_jobs.py new file mode 100644 index 0000000..854ae36 --- /dev/null +++ b/backend/alembic/versions/dcb7e2f03d2b_add_thumbnail_to_download_jobs.py @@ -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 ### diff --git a/backend/app/api/auto_download.py b/backend/app/api/auto_download.py index 2aab85e..def6037 100644 --- a/backend/app/api/auto_download.py +++ b/backend/app/api/auto_download.py @@ -69,7 +69,7 @@ async def process_download_job(job_id: int, db: AsyncSession, direct_url: str = file_exists=True, source_url=direct_url, source_type=source_type, - thumbnail=metadata.get("thumbnail") + thumbnail=metadata.get("thumbnail") or job.thumbnail ) db.add(db_music) @@ -237,7 +237,8 @@ async def create_download_job( # Create job job = DownloadJob( song_name=song_name, - status="pending" + status="pending", + thumbnail=request.thumbnail ) db.add(job) diff --git a/backend/app/models/models.py b/backend/app/models/models.py index 9439b4f..001ab59 100644 --- a/backend/app/models/models.py +++ b/backend/app/models/models.py @@ -109,6 +109,7 @@ class DownloadJob(Base): confirmed = Column(Boolean, default=False) # Whether user confirmed duplicate download is_duplicate = Column(Boolean, default=False) # Whether song already exists 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) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) diff --git a/backend/app/schemas/schemas.py b/backend/app/schemas/schemas.py index 47eb1a3..8eb1cf3 100644 --- a/backend/app/schemas/schemas.py +++ b/backend/app/schemas/schemas.py @@ -149,6 +149,7 @@ class DownloadJobCreate(BaseModel): song_name: str artist: Optional[str] = None # Artist name to improve search accuracy direct_url: Optional[str] = None # Direct URL to download (skips search) + thumbnail: Optional[str] = None # Thumbnail URL from search results class DownloadJobResponse(BaseModel): @@ -164,6 +165,7 @@ class DownloadJobResponse(BaseModel): confirmed: bool is_duplicate: bool duplicate_music_id: Optional[int] = None + thumbnail: Optional[str] = None created_at: datetime updated_at: datetime diff --git a/backend/app/services/downloader.py b/backend/app/services/downloader.py index 85eeef5..24edfe8 100644 --- a/backend/app/services/downloader.py +++ b/backend/app/services/downloader.py @@ -175,10 +175,20 @@ class MusicDownloader: files = [] for ext in ['.mp3', '.m4a', '.opus', '.webm']: if output_name: + # Try exact match first pattern = f"{output_name}{ext}" file_path = os.path.join(self.download_path, pattern) if os.path.exists(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: # Find most recent file for file in Path(self.download_path).glob(f"*{ext}"): diff --git a/dev-stack.sh b/dev-stack.sh index 4438d78..5c6d037 100755 --- a/dev-stack.sh +++ b/dev-stack.sh @@ -77,11 +77,11 @@ echo "" # 3. Backend Setup # ============================================================================ echo "🔧 Setting up Backend..." +cd backend # Create virtual environment if needed -if [ ! -d "backend/.venv" ]; then +if [ ! -d ".venv" ]; then echo "Creating Python 3.13 virtual environment..." - cd backend uv venv --python 3.13 cd .. fi @@ -91,11 +91,6 @@ echo "Installing Python dependencies with uv..." uv sync 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 "" diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 93622e4..2122ac4 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -100,8 +100,8 @@ export const settingsApi = { // Auto Download API export const autoDownloadApi = { - createJob: (songName: string, artist?: string, directUrl?: string) => - api.post('/auto-download/job', { song_name: songName, artist, direct_url: directUrl }), + createJob: (songName: string, artist?: string, directUrl?: string, thumbnail?: string) => + api.post('/auto-download/job', { song_name: songName, artist, direct_url: directUrl, thumbnail }), getJobs: (status?: string) => api.get('/auto-download/jobs', { params: { status } }), getJob: (jobId: number) => diff --git a/frontend/src/components/search/SearchPage.tsx b/frontend/src/components/search/SearchPage.tsx index 8073451..1e38a00 100644 --- a/frontend/src/components/search/SearchPage.tsx +++ b/frontend/src/components/search/SearchPage.tsx @@ -36,8 +36,8 @@ export default function SearchPage({ onPlayMusic }: SearchPageProps) { }) const downloadMutation = useMutation({ - mutationFn: ({ title, artist, url }: { title: string; artist?: string; url?: string }) => - autoDownloadApi.createJob(title, artist, url), + mutationFn: ({ title, artist, url, thumbnail }: { title: string; artist?: string; url?: string; thumbnail?: string }) => + autoDownloadApi.createJob(title, artist, url, thumbnail), onSuccess: () => { toast.success('Download job created! Check Download Center for progress.') }, @@ -51,13 +51,13 @@ export default function SearchPage({ onPlayMusic }: SearchPageProps) { setSearchQuery(query) } - const handleDownload = (title: string, artist?: string, url?: string) => { - downloadMutation.mutate({ title, artist, url }) + const handleDownload = (title: string, artist?: string, url?: string, thumbnail?: string) => { + downloadMutation.mutate({ title, artist, url, thumbnail }) } const handlePlayAndDownload = async (url: string, title: string, thumbnail?: string, artist?: string) => { // Start download job with direct URL - downloadMutation.mutate({ title, artist, url }) + downloadMutation.mutate({ title, artist, url, thumbnail }) // Create temporary music object for streaming playback const streamUrl = `/api/stream?url=${encodeURIComponent(url)}` @@ -185,7 +185,7 @@ export default function SearchPage({ onPlayMusic }: SearchPageProps) {