isgr9801 commited on
Commit
d458ace
·
verified ·
1 Parent(s): 789025d

Upload 16 files

Browse files
backend/README.md CHANGED
@@ -10,6 +10,12 @@ uvicorn backend.main:app --reload --port 8000
10
  ```
11
  - Replace the `auth` router with proper Firebase/JWT verification
12
 
 
 
 
 
 
 
13
  **NLP Processing**
14
  emotion scoring
15
  ----------------------------------
 
10
  ```
11
  - Replace the `auth` router with proper Firebase/JWT verification
12
 
13
+ **Spotify Recommendations**
14
+ ----------------------------
15
+ - `SPOTIFY_CLIENT_ID` = Spotify app client id
16
+ - `SPOTIFY_CLIENT_SECRET` = Spotify app client secret
17
+
18
+
19
  **NLP Processing**
20
  emotion scoring
21
  ----------------------------------
backend/crud.py CHANGED
@@ -73,7 +73,7 @@ def update_memory_by_id(memory_id: str, updates: dict) -> bool:
73
  {"_id": ObjectId(memory_id)},
74
  {"$set": updates}
75
  )
76
- return result.modified_count > 0
77
  except Exception:
78
  return False
79
 
 
73
  {"_id": ObjectId(memory_id)},
74
  {"$set": updates}
75
  )
76
+ return result.matched_count > 0
77
  except Exception:
78
  return False
79
 
backend/main.py CHANGED
@@ -10,7 +10,7 @@ from pathlib import Path
10
  load_dotenv(dotenv_path=Path(__file__).resolve().parent / ".env")
11
  load_dotenv()
12
 
13
- from backend.routers import auth, memories, dashboard
14
  from backend.nlp_processor import process_unprocessed_memories
15
 
16
  # Configure logging
@@ -55,6 +55,7 @@ def root():
55
  app.include_router(auth.router, prefix="/auth", tags=["auth"])
56
  app.include_router(memories.router, prefix="/memories", tags=["memories"])
57
  app.include_router(dashboard.router, prefix="/dashboard", tags=["dashboard"])
 
58
 
59
 
60
  # # Background scheduler for processing unprocessed memories
 
10
  load_dotenv(dotenv_path=Path(__file__).resolve().parent / ".env")
11
  load_dotenv()
12
 
13
+ from backend.routers import auth, memories, dashboard, spotify
14
  from backend.nlp_processor import process_unprocessed_memories
15
 
16
  # Configure logging
 
55
  app.include_router(auth.router, prefix="/auth", tags=["auth"])
56
  app.include_router(memories.router, prefix="/memories", tags=["memories"])
57
  app.include_router(dashboard.router, prefix="/dashboard", tags=["dashboard"])
58
+ app.include_router(spotify.router, prefix="/spotify", tags=["spotify"])
59
 
60
 
61
  # # Background scheduler for processing unprocessed memories
backend/routers/memories.py CHANGED
@@ -133,9 +133,13 @@ async def update_memory(memory_id: str, payload: schemas.MemoryUpdate, user: dic
133
  if not updates:
134
  raise HTTPException(status_code=400, detail="No updates provided")
135
 
 
 
 
 
136
  updated = crud.update_memory_by_id(memory_id, updates)
137
  if not updated:
138
- raise HTTPException(status_code=404, detail="Memory not found or unchanged")
139
 
140
  memory = crud.get_memory_by_id(memory_id)
141
  if not memory:
 
133
  if not updates:
134
  raise HTTPException(status_code=400, detail="No updates provided")
135
 
136
+ existing = crud.get_memory_by_id(memory_id)
137
+ if not existing:
138
+ raise HTTPException(status_code=404, detail="Memory not found")
139
+
140
  updated = crud.update_memory_by_id(memory_id, updates)
141
  if not updated:
142
+ raise HTTPException(status_code=500, detail="Failed to update memory")
143
 
144
  memory = crud.get_memory_by_id(memory_id)
145
  if not memory:
backend/routers/spotify.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import json
3
+ import os
4
+ from urllib.parse import urlencode
5
+ from urllib.error import HTTPError
6
+ from urllib.request import Request, urlopen
7
+
8
+ from fastapi import APIRouter, Depends, HTTPException
9
+
10
+ from backend import schemas
11
+ from backend.auth_deps import verify_firebase_token_optional
12
+
13
+ router = APIRouter()
14
+
15
+
16
+ SPOTIFY_USER_AGENT = "DigitalMemoryJar/1.0 (+https://localhost)"
17
+
18
+
19
+ def _spotify_token() -> str:
20
+ client_id = os.getenv("SPOTIFY_CLIENT_ID")
21
+ client_secret = os.getenv("SPOTIFY_CLIENT_SECRET")
22
+
23
+ if not client_id or not client_secret:
24
+ raise HTTPException(
25
+ status_code=503,
26
+ detail="Spotify is not configured. Add SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET.",
27
+ )
28
+
29
+ encoded = base64.b64encode(f"{client_id}:{client_secret}".encode("utf-8")).decode("utf-8")
30
+ data = urlencode({"grant_type": "client_credentials"}).encode("utf-8")
31
+
32
+ req = Request(
33
+ "https://accounts.spotify.com/api/token",
34
+ data=data,
35
+ headers={
36
+ "Authorization": f"Basic {encoded}",
37
+ "Content-Type": "application/x-www-form-urlencoded",
38
+ "Accept": "application/json",
39
+ "User-Agent": SPOTIFY_USER_AGENT,
40
+ },
41
+ method="POST",
42
+ )
43
+
44
+ try:
45
+ with urlopen(req, timeout=10) as response:
46
+ payload = json.loads(response.read().decode("utf-8"))
47
+ token = payload.get("access_token")
48
+ if not token:
49
+ raise HTTPException(status_code=502, detail="Failed to get Spotify access token")
50
+ return token
51
+ except HTTPError as exc:
52
+ try:
53
+ body = exc.read().decode("utf-8", "replace")
54
+ except Exception:
55
+ body = str(exc)
56
+ raise HTTPException(status_code=502, detail=f"Spotify token request failed: {exc.code} {body}") from exc
57
+ except HTTPException:
58
+ raise
59
+ except Exception as exc:
60
+ raise HTTPException(status_code=502, detail=f"Spotify token request failed: {exc}") from exc
61
+
62
+
63
+ def _build_query(payload: schemas.SpotifySuggestRequest) -> str:
64
+ mood = (payload.mood or "neutral").strip()
65
+ terms = [mood]
66
+ terms.extend([(k or "").strip() for k in payload.keywords[:3]])
67
+ terms.extend([(t or "").strip() for t in payload.topics[:2]])
68
+ filtered = [term for term in terms if term]
69
+ return " ".join(filtered) if filtered else "chill"
70
+
71
+
72
+ def _to_track(item: dict) -> schemas.SpotifyTrack | None:
73
+ external = item.get("external_urls") or {}
74
+ album = item.get("album") or {}
75
+ artists = item.get("artists") or []
76
+ artist_names = [artist.get("name", "") for artist in artists if artist.get("name")]
77
+ images = album.get("images") or []
78
+
79
+ url = external.get("spotify")
80
+ name = item.get("name")
81
+ if not url or not name:
82
+ return None
83
+
84
+ return schemas.SpotifyTrack(
85
+ title=name,
86
+ artist=", ".join(artist_names) if artist_names else "Unknown Artist",
87
+ url=url,
88
+ album_image=images[0].get("url") if images else None,
89
+ preview_url=item.get("preview_url"),
90
+ )
91
+
92
+
93
+ @router.post("/suggest", response_model=schemas.SpotifySuggestResponse)
94
+ async def spotify_suggest(
95
+ payload: schemas.SpotifySuggestRequest,
96
+ user: dict | None = Depends(verify_firebase_token_optional),
97
+ ):
98
+ token = _spotify_token()
99
+ query = _build_query(payload)
100
+ market = os.getenv("SPOTIFY_MARKET", "US")
101
+
102
+ params = urlencode(
103
+ {
104
+ "q": query,
105
+ "type": "track",
106
+ "limit": 5,
107
+ "market": market,
108
+ }
109
+ )
110
+
111
+ req = Request(
112
+ f"https://api.spotify.com/v1/search?{params}",
113
+ headers={
114
+ "Authorization": f"Bearer {token}",
115
+ "Accept": "application/json",
116
+ "User-Agent": SPOTIFY_USER_AGENT,
117
+ },
118
+ method="GET",
119
+ )
120
+
121
+ try:
122
+ with urlopen(req, timeout=10) as response:
123
+ payload_json = json.loads(response.read().decode("utf-8"))
124
+ except HTTPError as exc:
125
+ try:
126
+ body = exc.read().decode("utf-8", "replace")
127
+ except Exception:
128
+ body = str(exc)
129
+ raise HTTPException(status_code=502, detail=f"Spotify search failed: {exc.code} {body}") from exc
130
+ except Exception as exc:
131
+ raise HTTPException(status_code=502, detail=f"Spotify search failed: {exc}") from exc
132
+
133
+ items = ((payload_json.get("tracks") or {}).get("items") or [])
134
+ tracks = [track for track in (_to_track(item) for item in items) if track is not None]
135
+
136
+ if not tracks:
137
+ raise HTTPException(status_code=404, detail="No Spotify tracks found for this mood")
138
+
139
+ return schemas.SpotifySuggestResponse(
140
+ mood=(payload.mood or "neutral").lower(),
141
+ query=query,
142
+ primary=tracks[0],
143
+ alternatives=tracks[1:3],
144
+ )
backend/schemas.py CHANGED
@@ -65,3 +65,24 @@ class StatsResponse(BaseModel):
65
  most_common_mood: Optional[str] = None
66
  top_emotions: Optional[Dict[str, float]] = None
67
  top_topics: Optional[List[str]] = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  most_common_mood: Optional[str] = None
66
  top_emotions: Optional[Dict[str, float]] = None
67
  top_topics: Optional[List[str]] = None
68
+
69
+
70
+ class SpotifySuggestRequest(BaseModel):
71
+ mood: Optional[str] = Field("neutral", description="Detected mood")
72
+ keywords: List[str] = Field(default_factory=list, description="Top keywords from latest memory")
73
+ topics: List[str] = Field(default_factory=list, description="Top topics from latest memory")
74
+
75
+
76
+ class SpotifyTrack(BaseModel):
77
+ title: str
78
+ artist: str
79
+ url: str
80
+ album_image: Optional[str] = None
81
+ preview_url: Optional[str] = None
82
+
83
+
84
+ class SpotifySuggestResponse(BaseModel):
85
+ mood: str
86
+ query: str
87
+ primary: SpotifyTrack
88
+ alternatives: List[SpotifyTrack] = Field(default_factory=list)