spotify/server/dist/routes/users.js
2025-10-17 09:09:04 +02:00

129 lines
6.1 KiB
JavaScript

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 });
}
});