All checks were successful
Deploy production / deploy (push) Successful in 2m37s
- music schema: artists/albums/tracks (+album art), plays with source + dedupe, now_playing, spotify_auth, import_state; plays_deduped view hides Last.fm scrobbles Spotify also has - in-app Spotify poller (60s, leased) started from hooks init; no new role or env needed - /api/spotify/connect + callback (owner only), /trackers/music public stats - scripts: music-poll, music-import-lastfm (resumable), music-import-history - deploy/music-collector: optional host timer with its own role
48 lines
2.8 KiB
TypeScript
48 lines
2.8 KiB
TypeScript
import { error, redirect } from '@sveltejs/kit';
|
|
import { client } from '#lib/server/db/index.js';
|
|
import { requireOwner } from '#lib/server/owner.js';
|
|
import { exchangeCode, spotifyGet } from '#lib/music/collector.server.js';
|
|
import { CALLBACK_PATH, publicOrigin, safeEqual, STATE_COOKIE } from '#lib/server/music/oauth.js';
|
|
import { pollNow, spotifyCredentials } from '#lib/server/music/poller.js';
|
|
import type { RequestHandler } from './$types';
|
|
|
|
// Spotify redirects the owner here after /api/spotify/connect. Stores the refresh token in
|
|
// music.spotify_auth (app role only) and kicks off the first poll.
|
|
export const GET: RequestHandler = async (event) => {
|
|
requireOwner(event);
|
|
const { url, cookies, fetch } = event;
|
|
const expected = cookies.get(STATE_COOKIE) ?? '';
|
|
cookies.delete(STATE_COOKIE, { path: '/api/spotify' });
|
|
const state = url.searchParams.get('state') ?? '';
|
|
if (!expected || !safeEqual(state, expected)) error(400, 'Invalid or expired state, try again.');
|
|
if (url.searchParams.get('error'))
|
|
redirect(303, `/trackers/music?spotify=${encodeURIComponent(url.searchParams.get('error')!)}`);
|
|
const code = url.searchParams.get('code');
|
|
if (!code) error(400, 'Missing code');
|
|
const creds = spotifyCredentials();
|
|
if (!creds.clientId || !creds.clientSecret)
|
|
error(503, 'Spotify is not configured on this server.');
|
|
|
|
const tokens = await exchangeCode(fetch, creds, code, `${publicOrigin(url)}${CALLBACK_PATH}`);
|
|
if (!tokens.refresh_token) error(502, 'Spotify did not return a refresh token.');
|
|
const me = (await spotifyGet(fetch, tokens.access_token, 'me')) as Record<string, unknown> | null;
|
|
const userId = typeof me?.id === 'string' ? me.id : 'unknown';
|
|
const name = typeof me?.display_name === 'string' ? me.display_name : null;
|
|
await client`
|
|
insert into music.spotify_auth (id, spotify_user_id, display_name, refresh_token, access_token,
|
|
access_expires_at, scope, needs_reauth, last_error, connected_at, updated_at)
|
|
values (1, ${userId}, ${name}, ${tokens.refresh_token}, ${tokens.access_token},
|
|
now() + make_interval(secs => ${tokens.expires_in}), ${tokens.scope ?? null}, false, null, now(), now())
|
|
on conflict (id) do update set
|
|
spotify_user_id = excluded.spotify_user_id, display_name = excluded.display_name,
|
|
refresh_token = excluded.refresh_token, access_token = excluded.access_token,
|
|
access_expires_at = excluded.access_expires_at, scope = excluded.scope,
|
|
needs_reauth = false, last_error = null, lease_until = null,
|
|
-- another account starts a fresh recently-played cursor
|
|
recent_cursor = case when music.spotify_auth.spotify_user_id = excluded.spotify_user_id
|
|
then music.spotify_auth.recent_cursor end,
|
|
connected_at = now(), updated_at = now()`;
|
|
void pollNow();
|
|
redirect(303, '/trackers/music?spotify=connected');
|
|
};
|