- Fastify API with cookie (web) and bearer (mobile) sessions, follows, pinned friends, featured songs, account deletion, Redis-backed rate limits - Providers: Spotify (OAuth), Last.fm, ListenBrainz, Plex (PIN flow and server discovery), Jellyfin (token auth, revoked on disconnect) - Worker polls on an adaptive schedule in a Redis sorted set, so polling scales horizontally and API usage tracks whether anyone is watching - Track resolver links songs across services via ISRC (Deezer, Spotify), iTunes search and song.link - Realtime updates over WebSocket fan out through Redis pub/sub - User-supplied server URLs are restricted to public addresses, checked both up front and at connect time to defeat DNS rebinding Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
54 lines
2.3 KiB
TypeScript
54 lines
2.3 KiB
TypeScript
import { eq } from 'drizzle-orm';
|
|
import type { FastifyInstance } from 'fastify';
|
|
import { STREAMING_SERVICES, openUrl, type StreamingService, type TrackSearchResultDTO } from '@musicshare/shared';
|
|
import { db, schema } from '../db';
|
|
import { HttpError } from '../lib/http';
|
|
import { deezerSearch } from '../providers/catalog';
|
|
import { resolveTrackLinks } from '../services/resolver';
|
|
import { getTrackDTOs } from '../services/tracks';
|
|
|
|
export async function trackRoutes(app: FastifyInstance) {
|
|
app.get<{ Querystring: { q?: string } }>(
|
|
'/tracks/search',
|
|
{ config: { rateLimit: { max: 30, timeWindow: '1 minute' } } },
|
|
async (req): Promise<TrackSearchResultDTO[]> => {
|
|
const q = (req.query.q ?? '').trim();
|
|
if (q.length < 2) return [];
|
|
const results = await deezerSearch(q, 15);
|
|
return results.map((r) => ({
|
|
deezerId: String(r.id),
|
|
title: r.title,
|
|
artist: r.artist.name,
|
|
album: r.album.title,
|
|
artworkUrl: r.album.cover_big ?? null,
|
|
durationMs: r.duration * 1000,
|
|
}));
|
|
},
|
|
);
|
|
|
|
app.get<{ Params: { id: string } }>('/tracks/:id', async (req) => {
|
|
const track = (await getTrackDTOs([req.params.id])).get(req.params.id);
|
|
if (!track) throw new HttpError(404, 'Track not found');
|
|
return track;
|
|
});
|
|
|
|
/** Shareable redirect: /tracks/:id/open?service=spotify (defaults to the viewer's preferred service). */
|
|
app.get<{ Params: { id: string }; Querystring: { service?: string } }>('/tracks/:id/open', async (req, reply) => {
|
|
const service = (req.query.service ?? req.user?.preferredService ?? 'spotify') as StreamingService;
|
|
if (!STREAMING_SERVICES.includes(service)) throw new HttpError(400, 'Unknown service');
|
|
let track = (await getTrackDTOs([req.params.id])).get(req.params.id);
|
|
if (!track) throw new HttpError(404, 'Track not found');
|
|
if (!track.links[service]) {
|
|
const [row] = await db
|
|
.select({ resolved: schema.tracks.linksResolvedAt })
|
|
.from(schema.tracks)
|
|
.where(eq(schema.tracks.id, track.id));
|
|
if (!row?.resolved) {
|
|
await resolveTrackLinks(track.id).catch((e) => req.log.warn(e, 'inline resolve failed'));
|
|
track = (await getTrackDTOs([track.id])).get(track.id)!;
|
|
}
|
|
}
|
|
return reply.redirect(openUrl(track, service).url);
|
|
});
|
|
}
|