Set proper thumbnail

This commit is contained in:
2025-11-07 20:42:44 +11:00
parent 83d0dec2d1
commit 6b9d4d88c5
8 changed files with 77 additions and 18 deletions
@@ -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 ###
+3 -2
View File
@@ -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)
+1
View File
@@ -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)
+2
View File
@@ -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
+10
View File
@@ -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
View File
@@ -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 ""
+2 -2
View File
@@ -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" />