diff --git a/backend/app/api/endpoints/playlists.py b/backend/app/api/endpoints/playlists.py index ddc414d..bab7b1b 100644 --- a/backend/app/api/endpoints/playlists.py +++ b/backend/app/api/endpoints/playlists.py @@ -1,4 +1,5 @@ -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Query +from fastapi.responses import PlainTextResponse from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload @@ -49,6 +50,39 @@ async def get_playlist( return playlist +@router.get("/{playlist_id}/export") +async def export_playlist( + playlist_id: int, + format: str = Query("text", pattern="^(text|csv)$"), + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + result = await db.execute( + select(Playlist).options(selectinload(Playlist.tracks)) + .where(Playlist.id == playlist_id, Playlist.user_id == user.id) + ) + playlist = result.scalar_one_or_none() + if not playlist: + raise HTTPException(status_code=404, detail="Playlist not found") + + if format == "csv": + lines = ["Title,Artist,Album"] + for t in playlist.tracks: + # Escape commas in CSV + title = f'"{t.title}"' if ',' in t.title else t.title + artist = f'"{t.artist}"' if ',' in t.artist else t.artist + album = f'"{t.album}"' if t.album and ',' in t.album else (t.album or '') + lines.append(f"{title},{artist},{album}") + return PlainTextResponse("\n".join(lines), media_type="text/csv", + headers={"Content-Disposition": f'attachment; filename="{playlist.name}.csv"'}) + else: + lines = [f"{playlist.name}", "=" * len(playlist.name), ""] + for i, t in enumerate(playlist.tracks, 1): + lines.append(f"{i}. {t.artist} - {t.title}") + return PlainTextResponse("\n".join(lines), media_type="text/plain", + headers={"Content-Disposition": f'attachment; filename="{playlist.name}.txt"'}) + + @router.delete("/{playlist_id}") async def delete_playlist( playlist_id: int, diff --git a/backend/app/api/endpoints/recommendations.py b/backend/app/api/endpoints/recommendations.py index 760a7d0..6a5500e 100644 --- a/backend/app/api/endpoints/recommendations.py +++ b/backend/app/api/endpoints/recommendations.py @@ -1,9 +1,13 @@ import hashlib import json +import logging from urllib.parse import quote_plus import anthropic + +api_logger = logging.getLogger("app") from fastapi import APIRouter, Depends, HTTPException +from fastapi.responses import PlainTextResponse from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession @@ -77,6 +81,24 @@ async def history( return result.scalars().all() +@router.get("/saved/export") +async def export_saved(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)): + result = await db.execute( + select(Recommendation).where(Recommendation.user_id == user.id, Recommendation.saved == True) + .order_by(Recommendation.created_at.desc()) + ) + recs = result.scalars().all() + lines = ["My Saved Discoveries - Vynl", "=" * 30, ""] + for i, r in enumerate(recs, 1): + lines.append(f"{i}. {r.artist} - {r.title}") + if r.youtube_url: + lines.append(f" {r.youtube_url}") + lines.append(f" {r.reason}") + lines.append("") + return PlainTextResponse("\n".join(lines), media_type="text/plain", + headers={"Content-Disposition": 'attachment; filename="vynl-discoveries.txt"'}) + + @router.get("/saved", response_model=list[RecommendationItem]) async def saved( user: User = Depends(get_current_user), @@ -132,6 +154,12 @@ Return ONLY the JSON object.""" messages=[{"role": "user", "content": prompt}], ) + # Track API cost (Haiku: $0.80/M input, $4/M output) + input_tokens = message.usage.input_tokens + output_tokens = message.usage.output_tokens + cost = (input_tokens * 0.80 / 1_000_000) + (output_tokens * 4 / 1_000_000) + api_logger.info(f"API_COST|model=claude-haiku|input={input_tokens}|output={output_tokens}|cost=${cost:.4f}|user={user.id}|endpoint=analyze") + response_text = message.content[0].text.strip() if response_text.startswith("```"): response_text = response_text.split("\n", 1)[1] diff --git a/frontend/index.html b/frontend/index.html index 4c8430f..dcd0bb4 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -7,6 +7,21 @@ + + + + + + + + + + + + + + +