diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..80eddd8 --- /dev/null +++ b/app/main.py @@ -0,0 +1,94 @@ +import secrets +from spotipy.oauth2 import SpotifyOAuth +from dotenv import load_dotenv +import os +from fastapi import FastAPI, Form, Request, Depends +from fastapi.responses import RedirectResponse +from fastapi.middleware.cors import CORSMiddleware +from starlette.middleware.sessions import SessionMiddleware +from session_cache_handler import SessionCacheHandler +from fastapi.templating import Jinja2Templates +from typing import Annotated + +# Jinja2 template engine +templates = Jinja2Templates(directory="templates") + + +load_dotenv("../.env") +CLIENT_SECRET = os.environ.get("CLIENT_SECRET") +CLIENT_ID = os.environ.get("CLIENT_ID") +REDIRECT_URI = os.environ.get("REDIRECT_URI") + +app = FastAPI() # FastAPI instance +# Middlewares +app.add_middleware(SessionMiddleware, secret_key=secrets.token_urlsafe(32)) +# For more info on CORS: https://fastapi.tiangolo.com/tutorial/cors/?h=cors +origins = [ + "http://localhost:3000", + "http://localhost:8000", +] +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +# Dependencies +def get_spotify_oauth(request: Request): + return SpotifyOAuth( + scope="user-library-modify", + client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, + redirect_uri=REDIRECT_URI, + cache_handler=SessionCacheHandler(request.session), + ) + + +@app.get("/") +def index( + auth_manager: Annotated[SpotifyOAuth, Depends(get_spotify_oauth)], request: Request +): + # Check if we have a cached token + is_authenticated = bool(auth_manager.get_cached_token()) + # Render index.html template + return templates.TemplateResponse( + "index.html", {"request": request, "is_authenticated": is_authenticated} + ) + + +@app.get("/login") +def login(auth_manager: Annotated[SpotifyOAuth, Depends(get_spotify_oauth)]): + return RedirectResponse(auth_manager.get_authorize_url()) + + +@app.get("/callback") +async def callback( + auth_manager: Annotated[SpotifyOAuth, Depends(get_spotify_oauth)], request: Request +): + # Authorization code that spotify sends back https://developer.spotify.com/documentation/web-api/tutorials/code-flow + code = request.query_params.get("code") + if not code: + return {"Error": "No code provided"} + + # Exchange code for an access token + token_info = auth_manager.get_access_token(code) + + if not token_info: + return {"Error": "Could not retrieve token"} + + # Finally redirect to the index page + return RedirectResponse("/") + + +@app.post("/playlist") +def playlist(sentence: str = Form(...)): + return {"status": "ok"} + + +# Healthcheck endpoint to make sure the server is running +@app.get("/healthcheck") +def healthcheck(): + return {"status": "ok"} diff --git a/playlistfy.py b/app/playlistfy.py similarity index 68% rename from playlistfy.py rename to app/playlistfy.py index de2a9a3..7e6c5d5 100644 --- a/playlistfy.py +++ b/app/playlistfy.py @@ -1,6 +1,7 @@ import spotipy from dataclasses import dataclass + @dataclass class Playlist: name: str @@ -9,11 +10,15 @@ class Playlist: class Playlistify: - def __init__(self, spotify_client: spotipy.Spotify, playlist_name: str, playlist_description:str): + def __init__( + self, + spotify_client: spotipy.Spotify, + playlist_name: str, + playlist_description: str, + ): self.playlist_name = playlist_name self.playlist_description = playlist_description self.client = spotify_client - def create_playlist(self, sentence: str) -> Playlist: - pass \ No newline at end of file + pass diff --git a/session_cache_handler.py b/app/session_cache_handler.py similarity index 82% rename from session_cache_handler.py rename to app/session_cache_handler.py index f0e533e..054bd73 100644 --- a/session_cache_handler.py +++ b/app/session_cache_handler.py @@ -1,5 +1,6 @@ from spotipy import CacheHandler + class SessionCacheHandler(CacheHandler): def __init__(self, session): self.session = session @@ -8,4 +9,4 @@ def get_cached_token(self): return self.session.get("spotify_token_info") def save_token_to_cache(self, token_info): - self.session["spotify_token_info"] = token_info \ No newline at end of file + self.session["spotify_token_info"] = token_info diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..3b36192 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,43 @@ + + + + + + +
+
+
+
+
+

Welcome to the Home Page

+ {% if is_authenticated %} +
+ {% else %} + + {% endif %} +
+ + +
+
+ +
+
+
+
+
+
+
+ + diff --git a/app/test_main.py b/app/test_main.py new file mode 100644 index 0000000..9249bbb --- /dev/null +++ b/app/test_main.py @@ -0,0 +1,26 @@ +from fastapi.testclient import TestClient +from spotipy.oauth2 import SpotifyOAuth +from unittest.mock import Mock +from main import get_spotify_oauth + +from main import app + +client = TestClient(app, follow_redirects=False) + + +def test_login(): + mock_auth_manager = Mock(spec=SpotifyOAuth) + expected_url = ( + mock_auth_manager.get_authorize_url.return_value + ) = "http://example.com" + app.dependency_overrides[get_spotify_oauth] = lambda: mock_auth_manager + + response = client.get("/login") + assert response.status_code == 307 + assert response.headers["location"] == expected_url + + +def test_healthcheck(): + response = client.get("/healthcheck") + assert response.status_code == 200 + assert response.json() == {"status": "ok"} diff --git a/requirements.txt b/requirements.txt index 3d62b8a..1698c5e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,18 +1,33 @@ annotated-types==0.6.0 anyio==4.2.0 +black==23.12.1 certifi==2023.11.17 charset-normalizer==3.3.2 click==8.1.7 fastapi==0.109.0 +flake8==7.0.0 h11==0.14.0 +httpcore==1.0.2 httptools==0.6.1 +httpx==0.26.0 idna==3.6 +iniconfig==2.0.0 itsdangerous==2.1.2 Jinja2==3.1.3 MarkupSafe==2.1.4 +mccabe==0.7.0 +mypy-extensions==1.0.0 +packaging==23.2 +pathspec==0.12.1 +platformdirs==4.1.0 +pluggy==1.3.0 +pycodestyle==2.11.1 pydantic==2.5.3 pydantic_core==2.14.6 +pyflakes==3.2.0 +pytest==7.4.4 python-dotenv==1.0.0 +python-multipart==0.0.6 PyYAML==6.0.1 redis==5.0.1 requests==2.31.0