Fix YouTube Music import (run sync ytmusicapi in thread), remove all Spotify UI

This commit is contained in:
root
2026-03-31 00:30:55 -05:00
parent 1eea237c08
commit 50e9b492d5
8 changed files with 20 additions and 195 deletions

View File

@@ -1,3 +1,6 @@
import asyncio
from functools import partial
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import select from sqlalchemy import select
@@ -50,9 +53,12 @@ async def import_youtube_playlist(
detail="Free tier limited to 1 playlist. Upgrade to Pro for unlimited.", detail="Free tier limited to 1 playlist. Upgrade to Pro for unlimited.",
) )
# Fetch tracks from YouTube Music # Fetch tracks from YouTube Music (run sync ytmusicapi in thread)
loop = asyncio.get_event_loop()
try: try:
playlist_name, playlist_image, raw_tracks = get_playlist_tracks(data.url) playlist_name, playlist_image, raw_tracks = await loop.run_in_executor(
None, partial(get_playlist_tracks, data.url)
)
except ValueError as e: except ValueError as e:
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except Exception: except Exception:
@@ -102,8 +108,11 @@ async def search_youtube_music(
if not data.query.strip(): if not data.query.strip():
raise HTTPException(status_code=400, detail="Query cannot be empty") raise HTTPException(status_code=400, detail="Query cannot be empty")
loop = asyncio.get_event_loop()
try: try:
results = search_track(data.query.strip()) results = await loop.run_in_executor(
None, partial(search_track, data.query.strip())
)
except Exception: except Exception:
raise HTTPException(status_code=500, detail="Failed to search YouTube Music") raise HTTPException(status_code=500, detail="Failed to search YouTube Music")

View File

@@ -4,7 +4,6 @@ import Layout from './components/Layout'
import Landing from './pages/Landing' import Landing from './pages/Landing'
import Login from './pages/Login' import Login from './pages/Login'
import Register from './pages/Register' import Register from './pages/Register'
import SpotifyCallback from './pages/SpotifyCallback'
import Dashboard from './pages/Dashboard' import Dashboard from './pages/Dashboard'
import Playlists from './pages/Playlists' import Playlists from './pages/Playlists'
import PlaylistDetail from './pages/PlaylistDetail' import PlaylistDetail from './pages/PlaylistDetail'
@@ -33,7 +32,6 @@ function AppRoutes() {
<Route path="/" element={<RootRedirect />} /> <Route path="/" element={<RootRedirect />} />
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} /> <Route path="/register" element={<Register />} />
<Route path="/auth/spotify/callback" element={<SpotifyCallback />} />
<Route <Route
path="/dashboard" path="/dashboard"
element={ element={

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { Link, useNavigate } from 'react-router-dom' import { Link, useNavigate } from 'react-router-dom'
import { ListMusic, Heart, Sparkles, Compass, Loader2, Music, CheckCircle2, XCircle } from 'lucide-react' import { ListMusic, Heart, Sparkles, Compass, Loader2, Music } from 'lucide-react'
import { useAuth } from '../lib/auth' import { useAuth } from '../lib/auth'
import { getPlaylists, getRecommendationHistory, getSavedRecommendations, generateRecommendations, type RecommendationItem, type PlaylistResponse } from '../lib/api' import { getPlaylists, getRecommendationHistory, getSavedRecommendations, generateRecommendations, type RecommendationItem, type PlaylistResponse } from '../lib/api'
import RecommendationCard from '../components/RecommendationCard' import RecommendationCard from '../components/RecommendationCard'
@@ -162,27 +162,6 @@ export default function Dashboard() {
</div> </div>
</div> </div>
{/* Connected Accounts */}
<div className="bg-white rounded-2xl border border-purple-100 p-6">
<h2 className="text-lg font-semibold text-charcoal mb-4">Connected Accounts</h2>
<div className="flex items-center gap-3">
<div className="flex items-center gap-3 px-4 py-3 bg-cream rounded-xl flex-1">
<svg className="w-5 h-5 text-[#1DB954]" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z" />
</svg>
<span className="text-sm font-medium text-charcoal">Spotify</span>
{user?.spotify_connected ? (
<CheckCircle2 className="w-4 h-4 text-green-500 ml-auto" />
) : (
<XCircle className="w-4 h-4 text-charcoal-muted/40 ml-auto" />
)}
<span className="text-xs text-charcoal-muted">
{user?.spotify_connected ? 'Connected' : 'Not connected'}
</span>
</div>
</div>
</div>
{/* Recent Recommendations */} {/* Recent Recommendations */}
{recentRecs.length > 0 && ( {recentRecs.length > 0 && (
<div> <div>

View File

@@ -5,7 +5,7 @@ const features = [
{ {
icon: ListMusic, icon: ListMusic,
title: 'Import Your Music', title: 'Import Your Music',
description: 'Connect Spotify and import your playlists to build your taste profile.', description: 'Import your playlists from YouTube Music, Last.fm, or paste your songs to build your taste profile.',
}, },
{ {
icon: Sparkles, icon: Sparkles,

View File

@@ -2,7 +2,7 @@ import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom' import { Link, useNavigate } from 'react-router-dom'
import { Disc3, Mail, Lock, Loader2 } from 'lucide-react' import { Disc3, Mail, Lock, Loader2 } from 'lucide-react'
import { useAuth } from '../lib/auth' import { useAuth } from '../lib/auth'
import { login as apiLogin, getSpotifyAuthUrl } from '../lib/api' import { login as apiLogin } from '../lib/api'
export default function Login() { export default function Login() {
const [email, setEmail] = useState('') const [email, setEmail] = useState('')
@@ -28,15 +28,6 @@ export default function Login() {
} }
} }
const handleSpotifyLogin = async () => {
try {
const { url } = await getSpotifyAuthUrl()
window.location.href = url
} catch {
setError('Could not connect to Spotify')
}
}
return ( return (
<div className="min-h-screen bg-cream flex flex-col"> <div className="min-h-screen bg-cream flex flex-col">
{/* Header */} {/* Header */}
@@ -113,22 +104,6 @@ export default function Login() {
</button> </button>
</form> </form>
<div className="my-6 flex items-center gap-3">
<div className="flex-1 h-px bg-purple-100" />
<span className="text-xs text-charcoal-muted uppercase tracking-wider">or</span>
<div className="flex-1 h-px bg-purple-100" />
</div>
<button
onClick={handleSpotifyLogin}
className="w-full py-3 bg-[#1DB954] text-white font-semibold rounded-xl hover:bg-[#1aa34a] transition-colors cursor-pointer border-none text-sm flex items-center justify-center gap-2"
>
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z" />
</svg>
Continue with Spotify
</button>
<p className="text-center mt-6 text-sm text-charcoal-muted"> <p className="text-center mt-6 text-sm text-charcoal-muted">
Don't have an account?{' '} Don't have an account?{' '}
<Link to="/register" className="text-purple font-medium hover:underline"> <Link to="/register" className="text-purple font-medium hover:underline">

View File

@@ -1,17 +1,13 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { ListMusic, Plus, Loader2, Music, ChevronRight, Download, X, Play, Link2, ClipboardPaste } from 'lucide-react' import { ListMusic, Loader2, Music, ChevronRight, Download, X, Play, Link2, ClipboardPaste } from 'lucide-react'
import { getPlaylists, getSpotifyPlaylists, importSpotifyPlaylist, importYouTubePlaylist, previewLastfm, importLastfm, importPastedSongs, type PlaylistResponse, type SpotifyPlaylistItem, type LastfmPreviewResponse } from '../lib/api' import { getPlaylists, importYouTubePlaylist, previewLastfm, importLastfm, importPastedSongs, type PlaylistResponse, type LastfmPreviewResponse } from '../lib/api'
export default function Playlists() { export default function Playlists() {
const [playlists, setPlaylists] = useState<PlaylistResponse[]>([]) const [playlists, setPlaylists] = useState<PlaylistResponse[]>([])
const [spotifyPlaylists, setSpotifyPlaylists] = useState<SpotifyPlaylistItem[]>([])
const [showImport, setShowImport] = useState(false)
const [showYouTubeImport, setShowYouTubeImport] = useState(false) const [showYouTubeImport, setShowYouTubeImport] = useState(false)
const [youtubeUrl, setPlayUrl] = useState('') const [youtubeUrl, setPlayUrl] = useState('')
const [importingYouTube, setImportingYouTube] = useState(false) const [importingYouTube, setImportingYouTube] = useState(false)
const [importing, setImporting] = useState<string | null>(null)
const [loadingSpotify, setLoadingSpotify] = useState(false)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState('') const [error, setError] = useState('')
const [showLastfmImport, setShowLastfmImport] = useState(false) const [showLastfmImport, setShowLastfmImport] = useState(false)
@@ -40,19 +36,6 @@ export default function Playlists() {
} }
} }
const openImportModal = async () => {
setShowImport(true)
setLoadingSpotify(true)
try {
const data = await getSpotifyPlaylists()
setSpotifyPlaylists(data)
} catch {
setError('Failed to load Spotify playlists. Make sure your Spotify account is connected.')
} finally {
setLoadingSpotify(false)
}
}
const handleYouTubeImport = async () => { const handleYouTubeImport = async () => {
if (!youtubeUrl.trim()) return if (!youtubeUrl.trim()) return
setImportingYouTube(true) setImportingYouTube(true)
@@ -122,19 +105,6 @@ export default function Playlists() {
.split('\n') .split('\n')
.filter((line) => line.trim().length > 0).length .filter((line) => line.trim().length > 0).length
const handleImport = async (playlistId: string) => {
setImporting(playlistId)
try {
const imported = await importSpotifyPlaylist(playlistId)
setPlaylists((prev) => [...prev, imported])
setSpotifyPlaylists((prev) => prev.filter((p) => p.id !== playlistId))
} catch (err: any) {
setError(err.response?.data?.detail || 'Failed to import playlist')
} finally {
setImporting(null)
}
}
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center py-20"> <div className="flex items-center justify-center py-20">
@@ -151,13 +121,6 @@ export default function Playlists() {
<p className="text-charcoal-muted mt-1">Manage your imported playlists</p> <p className="text-charcoal-muted mt-1">Manage your imported playlists</p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button
onClick={openImportModal}
className="flex items-center gap-2 px-5 py-2.5 bg-purple text-white font-medium rounded-xl hover:bg-purple-dark transition-colors cursor-pointer border-none text-sm"
>
<Plus className="w-4 h-4" />
Import from Spotify
</button>
<button <button
onClick={() => setShowYouTubeImport(true)} onClick={() => setShowYouTubeImport(true)}
className="flex items-center gap-2 px-5 py-2.5 bg-red-600 text-white font-medium rounded-xl hover:bg-red-700 transition-colors cursor-pointer border-none text-sm" className="flex items-center gap-2 px-5 py-2.5 bg-red-600 text-white font-medium rounded-xl hover:bg-red-700 transition-colors cursor-pointer border-none text-sm"
@@ -196,16 +159,9 @@ export default function Playlists() {
</div> </div>
<h2 className="text-xl font-semibold text-charcoal mb-2">No playlists yet</h2> <h2 className="text-xl font-semibold text-charcoal mb-2">No playlists yet</h2>
<p className="text-charcoal-muted mb-6 max-w-md mx-auto"> <p className="text-charcoal-muted mb-6 max-w-md mx-auto">
Import your playlists from Spotify or YouTube Music to start getting personalized music recommendations Import your playlists from YouTube Music, Last.fm, or paste your songs to start getting personalized music recommendations
</p> </p>
<div className="flex items-center justify-center gap-3"> <div className="flex items-center justify-center gap-3">
<button
onClick={openImportModal}
className="inline-flex items-center gap-2 px-6 py-3 bg-purple text-white font-medium rounded-xl hover:bg-purple-dark transition-colors cursor-pointer border-none text-sm"
>
<Download className="w-4 h-4" />
Import from Spotify
</button>
<button <button
onClick={() => setShowYouTubeImport(true)} onClick={() => setShowYouTubeImport(true)}
className="inline-flex items-center gap-2 px-6 py-3 bg-red-600 text-white font-medium rounded-xl hover:bg-red-700 transition-colors cursor-pointer border-none text-sm" className="inline-flex items-center gap-2 px-6 py-3 bg-red-600 text-white font-medium rounded-xl hover:bg-red-700 transition-colors cursor-pointer border-none text-sm"
@@ -488,73 +444,6 @@ export default function Playlists() {
</div> </div>
)} )}
{/* Import Modal */}
{showImport && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-xl max-w-lg w-full max-h-[80vh] overflow-hidden">
<div className="flex items-center justify-between px-6 py-4 border-b border-purple-100">
<h2 className="text-lg font-semibold text-charcoal">Import from Spotify</h2>
<button
onClick={() => setShowImport(false)}
className="p-1.5 rounded-lg hover:bg-purple-50 transition-colors cursor-pointer bg-transparent border-none"
>
<X className="w-5 h-5 text-charcoal-muted" />
</button>
</div>
<div className="overflow-y-auto max-h-[60vh] p-4">
{loadingSpotify ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-6 h-6 text-purple animate-spin" />
</div>
) : spotifyPlaylists.length === 0 ? (
<div className="text-center py-12">
<p className="text-charcoal-muted">No playlists found on Spotify</p>
</div>
) : (
<div className="space-y-2">
{spotifyPlaylists.map((sp) => (
<div
key={sp.id}
className="flex items-center gap-3 p-3 rounded-xl hover:bg-purple-50 transition-colors"
>
<div className="w-12 h-12 rounded-lg bg-gradient-to-br from-purple-200 to-purple-400 flex-shrink-0 flex items-center justify-center overflow-hidden">
{sp.image_url ? (
<img
src={sp.image_url}
alt={sp.name}
className="w-full h-full object-cover"
/>
) : (
<Music className="w-5 h-5 text-white/80" />
)}
</div>
<div className="flex-1 min-w-0">
<p className="font-medium text-charcoal text-sm truncate">{sp.name}</p>
<p className="text-xs text-charcoal-muted">
{sp.track_count} tracks &middot; {sp.owner}
</p>
</div>
<button
onClick={() => handleImport(sp.id)}
disabled={importing === sp.id}
className="px-4 py-2 bg-purple text-white text-xs font-medium rounded-lg hover:bg-purple-dark transition-colors cursor-pointer border-none disabled:opacity-50 flex items-center gap-1.5"
>
{importing === sp.id ? (
<Loader2 className="w-3 h-3 animate-spin" />
) : (
<Download className="w-3 h-3" />
)}
Import
</button>
</div>
))}
</div>
)}
</div>
</div>
</div>
)}
</div> </div>
) )
} }

View File

@@ -2,7 +2,7 @@ import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom' import { Link, useNavigate } from 'react-router-dom'
import { Disc3, Mail, Lock, User, Loader2 } from 'lucide-react' import { Disc3, Mail, Lock, User, Loader2 } from 'lucide-react'
import { useAuth } from '../lib/auth' import { useAuth } from '../lib/auth'
import { register as apiRegister, getSpotifyAuthUrl } from '../lib/api' import { register as apiRegister } from '../lib/api'
export default function Register() { export default function Register() {
const [name, setName] = useState('') const [name, setName] = useState('')
@@ -29,15 +29,6 @@ export default function Register() {
} }
} }
const handleSpotifyLogin = async () => {
try {
const { url } = await getSpotifyAuthUrl()
window.location.href = url
} catch {
setError('Could not connect to Spotify')
}
}
return ( return (
<div className="min-h-screen bg-cream flex flex-col"> <div className="min-h-screen bg-cream flex flex-col">
{/* Header */} {/* Header */}
@@ -133,22 +124,6 @@ export default function Register() {
</button> </button>
</form> </form>
<div className="my-6 flex items-center gap-3">
<div className="flex-1 h-px bg-purple-100" />
<span className="text-xs text-charcoal-muted uppercase tracking-wider">or</span>
<div className="flex-1 h-px bg-purple-100" />
</div>
<button
onClick={handleSpotifyLogin}
className="w-full py-3 bg-[#1DB954] text-white font-semibold rounded-xl hover:bg-[#1aa34a] transition-colors cursor-pointer border-none text-sm flex items-center justify-center gap-2"
>
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z" />
</svg>
Continue with Spotify
</button>
<p className="text-center mt-6 text-sm text-charcoal-muted"> <p className="text-center mt-6 text-sm text-charcoal-muted">
Already have an account?{' '} Already have an account?{' '}
<Link to="/login" className="text-purple font-medium hover:underline"> <Link to="/login" className="text-purple font-medium hover:underline">

View File

@@ -93,7 +93,7 @@ export default function TasteProfilePage() {
<h2 className="text-lg font-semibold text-charcoal mb-5">Genre DNA</h2> <h2 className="text-lg font-semibold text-charcoal mb-5">Genre DNA</h2>
{profile.genre_breakdown.length === 0 ? ( {profile.genre_breakdown.length === 0 ? (
<p className="text-charcoal-muted text-sm"> <p className="text-charcoal-muted text-sm">
No genre data yet. Import playlists with Spotify to see your genre breakdown. No genre data yet. Import playlists to see your genre breakdown.
</p> </p>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">