Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 35 additions & 16 deletions UI_Front/src/components/SpotifyPlaylistGenerator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { authService, playlistService } from "@/lib/api";
import type { Track } from "@/lib/api";
import { usePreferences } from "@/context/PreferencesContext";
import { useToast } from "@/hooks/use-toast";

type AppStep = "landing" | "auth" | "preferences" | "preview" | "success";

Expand All @@ -42,6 +43,7 @@ const LOCAL_STORAGE_KEY = "spotify_playlist_generator_state";

const SpotifyPlaylistGenerator = () => {
const { preferences, setPreferences } = usePreferences();
const { toast } = useToast();
const [currentStep, setCurrentStep] = useState<AppStep>("landing");
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
Expand Down Expand Up @@ -121,6 +123,12 @@ const SpotifyPlaylistGenerator = () => {
} catch (error) {
console.error("Auth check failed:", error);
setIsAuthenticated(false);
setCurrentStep("landing");
toast({
variant: "destructive",
title: "Could not verify your Spotify session",
description: error instanceof Error ? error.message : "Please log in again.",
});
} finally {
setIsCheckingAuth(false);
}
Expand Down Expand Up @@ -154,24 +162,22 @@ const SpotifyPlaylistGenerator = () => {
};

const handleGeneratePlaylist = (playlist: GeneratedPlaylist) => {
try {
console.log("Received generated playlist:", playlist);
// The API call is already made in PreferenceInput component
// This function just updates the state with the received playlist
if (!playlist || !playlist.Tracks) {
console.error("Invalid playlist data received:", playlist);
return;
}
setGeneratedPlaylist(playlist);
setCurrentStep("preview");
} catch (error) {
console.error("Error handling generated playlist:", error);
// Don't throw - the playlist was already created successfully
// The API call is already made in PreferenceInput component
// This function just updates the state with the received playlist
if (!playlist || !playlist.Tracks) {
console.error("Invalid playlist data received:", playlist);
toast({
variant: "destructive",
title: "Could not load the generated playlist",
description: "The server returned an unexpected response. Please try generating again.",
});
return;
}
setGeneratedPlaylist(playlist);
setCurrentStep("preview");
};

const handleSavePlaylist = async (name: string, description: string, tracks: Track[]) => {
console.log("Saving playlist:", name, tracks);
try {
const uris = tracks.map(t => t.uri);
const response = await playlistService.createPlaylist(name, description, uris);
Expand All @@ -182,7 +188,11 @@ const SpotifyPlaylistGenerator = () => {
setCurrentStep("success");
} catch (error) {
console.error("Failed to save playlist:", error);
// Handle error (show toast?)
toast({
variant: "destructive",
title: "Failed to save playlist",
description: error instanceof Error ? error.message : "Please try again.",
});
}
};

Expand All @@ -206,7 +216,16 @@ const SpotifyPlaylistGenerator = () => {
};

const handleBackHome = async () => {
await authService.logout();
try {
await authService.logout();
} catch (error) {
console.error("Logout failed:", error);
toast({
variant: "destructive",
title: "Logout failed on the server",
description: "Your local session was cleared, but you may still be signed in to Spotify.",
});
}

setIsAuthenticated(false);
setCurrentStep("landing");
Expand Down
31 changes: 28 additions & 3 deletions backend/app.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,40 @@
import logging
import os

from flask import Flask
from flask_session import Session
from flask_cors import CORS

from .config import Config
from .errors import register_error_handlers

logger = logging.getLogger(__name__)


def create_app():
logging.basicConfig(
level=os.getenv("LOG_LEVEL", "INFO"),
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)

app = Flask(__name__)
app.config.from_object(Config)

# Surface misconfiguration at startup instead of on the first request.
missing = Config.missing_settings()
if missing:
logger.error(
"Missing required environment variables: %s. "
"Login and playlist generation will fail until they are set.",
", ".join(missing),
)

os.makedirs(app.config['SESSION_FILE_DIR'], exist_ok=True)

# Initialize Session
Session(app)

# Initialize CORS
# Allowing specific origins as per main2.py but simplified
CORS(app,
resources={r"/*": {"origins": ["http://localhost:8080", "http://127.0.0.1:8080", "http://localhost:8081", "http://127.0.0.1:8081"]}},
supports_credentials=True)
Expand All @@ -22,7 +45,9 @@ def create_app():

app.register_blueprint(auth_bp)
app.register_blueprint(playlist_bp)


register_error_handlers(app)

@app.route('/')
def health_check():
return {"status": "ok", "service": "Spotify AI Backend"}
Expand Down
25 changes: 18 additions & 7 deletions backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from datetime import timedelta
from dotenv import load_dotenv

from .errors import ConfigError

load_dotenv()

class Config:
Expand Down Expand Up @@ -44,14 +46,23 @@ class Config:
# AI / Gemini
GENAI_API_KEY = os.getenv("GENAI_API_KEY")

@classmethod
def missing_settings(cls):
"""Return the names of required environment variables that are unset."""
required = {
"CLIENT_ID": cls.SPOTIFY_CLIENT_ID,
"CLIENT_SECRET": cls.SPOTIFY_CLIENT_SECRET,
"REDIRECT_URI": cls.SPOTIFY_REDIRECT_URI,
"GENAI_API_KEY": cls.GENAI_API_KEY,
}
return [name for name, value in required.items() if not value]

@classmethod
def validate(cls):
"""Ensure critical config exists"""
missing = []
if not cls.SPOTIFY_CLIENT_ID: missing.append("CLIENT_ID")
if not cls.SPOTIFY_CLIENT_SECRET: missing.append("CLIENT_SECRET")
if not cls.SPOTIFY_REDIRECT_URI: missing.append("REDIRECT_URI")
if not cls.GENAI_API_KEY: missing.append("GENAI_API_KEY")

missing = cls.missing_settings()
if missing:
raise ValueError(f"Missing required environment variables: {', '.join(missing)}")
raise ConfigError(
"Server misconfiguration",
details=f"Missing required environment variables: {', '.join(missing)}",
)
74 changes: 74 additions & 0 deletions backend/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Application error types and JSON error handlers."""

import logging

from flask import jsonify
from werkzeug.exceptions import HTTPException

logger = logging.getLogger(__name__)


class AppError(Exception):
"""Base class for errors that map onto an HTTP JSON response."""

status_code = 500
message = "Internal server error"

def __init__(self, message=None, status_code=None, details=None):
super().__init__(message or self.message)
if message:
self.message = message
if status_code:
self.status_code = status_code
self.details = details

def to_dict(self):
payload = {"error": self.message}
if self.details:
payload["details"] = self.details
return payload


class ConfigError(AppError):
status_code = 500
message = "Server misconfiguration"


class SpotifyAuthError(AppError):
"""The Spotify access token is missing, expired or rejected."""

status_code = 401
message = "Spotify authentication expired, please log in again"


class SpotifyRateLimitError(AppError):
status_code = 429
message = "Rate limited by Spotify, please retry shortly"


class SpotifyAPIError(AppError):
status_code = 502
message = "Spotify request failed"


class AIServiceError(AppError):
status_code = 502
message = "AI playlist generation failed"


def register_error_handlers(app):
@app.errorhandler(AppError)
def handle_app_error(err):
logger.warning(
"%s: %s (details=%s)", type(err).__name__, err.message, err.details
)
return jsonify(err.to_dict()), err.status_code

@app.errorhandler(HTTPException)
def handle_http_error(err):
return jsonify({"error": err.description}), err.code

@app.errorhandler(Exception)
def handle_unexpected_error(err):
logger.exception("Unhandled exception while serving request: %s", err)
return jsonify({"error": "Internal server error"}), 500
Loading