import { Router } from 'express'; import { db } from '../lib/db.js'; import axios from 'axios'; import { requireAuth } from '../middleware/auth.js'; import { syncUserData } from '../lib/sync.js'; import { ensureValidAccessToken } from '../lib/spotify.js'; export const usersRouter = Router(); // Fetch and store user data (recently played, top tracks/artists) usersRouter.post('/:uid/sync', requireAuth, async (req, res) => { try { const { uid } = req.params; if (!req.user || req.user.uid !== uid) return res.status(403).json({ error: 'Forbidden' }); await syncUserData(uid); res.json({ ok: true }); } catch (err) { console.error(err.response?.data || err.message); res.status(500).json({ error: 'Sync failed' }); } }); usersRouter.get('/:uid', (req, res) => { const { uid } = req.params; const user = db.db.prepare('SELECT id, display_name, email, avatar_url, country, product, created_at, updated_at, last_synced_at FROM users WHERE id = ?').get(uid); if (!user) return res.status(404).json({ error: 'Not found' }); res.json(user); }); // Simple search by display_name (substring, case-insensitive) usersRouter.get('/search/q', (req, res) => { const q = String(req.query.q || '').trim(); if (!q) return res.json([]); const rows = db.db .prepare('SELECT id, display_name, avatar_url FROM users WHERE lower(display_name) LIKE ? LIMIT 20') .all(`%${q.toLowerCase()}%`); res.json(rows); }); usersRouter.get('/:uid/recently-played', (req, res) => { const { uid } = req.params; const rows = db.db.prepare('SELECT id, user_id, played_at, track_json FROM recently_played WHERE user_id = ? ORDER BY played_at DESC LIMIT 50').all(uid); res.json(rows.map((r) => ({ id: r.id, user_id: r.user_id, played_at: r.played_at, track: JSON.parse(r.track_json) }))); }); usersRouter.get('/:uid/top-tracks', (req, res) => { const { uid } = req.params; const range = req.query.time_range || 'short_term'; const rows = db.db.prepare('SELECT rank, track_json FROM top_tracks WHERE user_id = ? AND time_range = ? ORDER BY rank ASC').all(uid, range); res.json(rows.map((r) => ({ rank: r.rank, track: JSON.parse(r.track_json) }))); }); usersRouter.get('/:uid/top-artists', (req, res) => { const { uid } = req.params; const range = req.query.time_range || 'short_term'; const rows = db.db.prepare('SELECT rank, artist_json FROM top_artists WHERE user_id = ? AND time_range = ? ORDER BY rank ASC').all(uid, range); res.json(rows.map((r) => ({ rank: r.rank, artist: JSON.parse(r.artist_json) }))); }); // status: last sync time and next scheduled time (based on 10 min interval) usersRouter.get('/:uid/status', (req, res) => { const { uid } = req.params; const row = db.db.prepare('SELECT last_synced_at FROM users WHERE id = ?').get(uid); const last = row?.last_synced_at || null; // Match backend full sync interval (2 minutes) const next = last ? last + 2 * 60 * 1000 : null; res.json({ lastSyncedAt: last, nextSyncAt: next }); }); // Now playing (with token refresh) usersRouter.get('/:uid/now-playing', requireAuth, async (req, res) => { try { const { uid } = req.params; if (!req.user) return res.status(403).json({ error: 'Forbidden' }); if (req.user.uid !== uid) { // allow if users are partnered const paired = db.db.prepare('SELECT 1 FROM friendships WHERE (user_a_id=? AND user_b_id=?) OR (user_a_id=? AND user_b_id=?)').get(req.user.uid, uid, uid, req.user.uid); if (!paired) return res.status(403).json({ error: 'Forbidden' }); } // Try DB-captured snapshot first (last 60s) const recent = db.db.prepare('SELECT played_at, track_json FROM recently_played WHERE user_id=? ORDER BY played_at DESC LIMIT 1').get(uid); if (recent?.played_at && Date.now() - recent.played_at < 60000 && recent?.track_json) { try { const item = JSON.parse(recent.track_json); return res.json({ is_playing: true, item, source: 'db' }); } catch { } } const accessToken = await ensureValidAccessToken(uid); const resp = await axios.get('https://api.spotify.com/v1/me/player/currently-playing?additional_types=track', { headers: { Authorization: `Bearer ${accessToken}` }, validateStatus: () => true, }); if (resp.status === 204) return res.json({ is_playing: false }); if (resp.status >= 400) { // Return a benign payload rather than 500 to avoid breaking UI when Spotify returns errors return res.json({ is_playing: false }); } return res.status(200).json(resp.data); } catch (e) { res.status(200).json({ is_playing: false }); } }); // Audio features bulk usersRouter.get('/:uid/audio-features', requireAuth, async (req, res) => { try { const { uid } = req.params; const ids = String(req.query.ids || '').split(',').map(s => s.trim()).filter(Boolean); if (!req.user) return res.status(403).json({ error: 'Forbidden' }); if (req.user.uid !== uid) { const paired = db.db.prepare('SELECT 1 FROM friendships WHERE (user_a_id=? AND user_b_id=?) OR (user_a_id=? AND user_b_id=?)').get(req.user.uid, uid, uid, req.user.uid); if (!paired) return res.status(403).json({ error: 'Forbidden' }); } if (!ids.length) return res.json({ audio_features: [] }); const accessToken = await ensureValidAccessToken(uid); // Some tracks may be local or missing features; handle gracefully const resp = await axios.get('https://api.spotify.com/v1/audio-features', { headers: { Authorization: `Bearer ${accessToken}` }, params: { ids: ids.slice(0, 100).join(',') }, }); res.json({ audio_features: Array.isArray(resp.data?.audio_features) ? resp.data.audio_features.filter(Boolean) : [] }); } catch (e) { res.status(500).json({ error: 'Failed to fetch audio features', detail: e?.response?.data || e?.message }); } });