Files
musicshare/apps/api/src/routes/tracks.ts
T
chrisandClaude Opus 5.5 6791b62d87 Add API and worker: accounts, follows, now-playing, cross-service links
- 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
2026-10-03 09:59:02 -04:00

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